From d2fc7afa5915e8ff32c846c9006b43a4444f525d Mon Sep 17 00:00:00 2001 From: dvab-sarma Date: Thu, 26 Jun 2025 23:48:13 -0500 Subject: [PATCH] android-16.0 --- .github/workflows/build.yml | 171 + .gitignore | 81 + Android.bp | 140 + COPYING | 340 ++ ChangeLog | 43 + INSTALL | 56 + Makefile.am | 57 + README.md | 28 + TODO | 4 + acinclude.m4 | 12 + alsa-info/Makefile.am | 3 + alsa-info/alsa-info.sh | 1019 +++++ alsa-info/alsa-info.sh.8 | 62 + alsaconf/Makefile.am | 37 + alsaconf/alsaconf.8 | 102 + alsaconf/alsaconf.fr.8 | 111 + alsaconf/alsaconf.in | 1451 +++++++ alsaconf/po/eu.po | 381 ++ alsaconf/po/ja.po | 362 ++ alsaconf/po/ka.po | 378 ++ alsaconf/po/ru.po | 373 ++ alsactl/.gitignore | 3 + alsactl/90-alsa-restore.rules.in | 30 + alsactl/Makefile.am | 76 + alsactl/alsa-restore.service.in | 15 + alsactl/alsa-state.service.in | 13 + alsactl/alsactl.1 | 231 ++ alsactl/alsactl.c | 495 +++ alsactl/alsactl.h | 92 + alsactl/alsactl_init.xml | 633 +++ alsactl/clean.c | 196 + alsactl/daemon.c | 461 +++ alsactl/info.c | 268 ++ alsactl/init/00main | 49 + alsactl/init/Makefile.am | 7 + alsactl/init/ca0106 | 31 + alsactl/init/default | 284 ++ alsactl/init/hda | 39 + alsactl/init/help | 8 + alsactl/init/info | 22 + alsactl/init/test | 270 ++ alsactl/init_parse.c | 1787 +++++++++ alsactl/init_sysdeps.c | 65 + alsactl/init_sysfs.c | 163 + alsactl/init_ucm.c | 80 + alsactl/init_utils_run.c | 247 ++ alsactl/init_utils_string.c | 183 + alsactl/list.h | 289 ++ alsactl/lock.c | 211 + alsactl/monitor.c | 452 +++ alsactl/state.c | 1777 ++++++++ alsactl/utils.c | 334 ++ alsaloop/Makefile.am | 14 + alsaloop/alsaloop.1 | 213 + alsaloop/alsaloop.c | 952 +++++ alsaloop/alsaloop.h | 221 + alsaloop/control.c | 426 ++ alsaloop/effect-sweep.c | 129 + alsaloop/pcmjob.c | 2095 ++++++++++ alsaloop/test.sh | 94 + alsamixer/Makefile.am | 30 + alsamixer/alsamixer.1 | 400 ++ alsamixer/alsamixer.rc.example | 192 + alsamixer/bindings.c | 132 + alsamixer/bindings.h | 81 + alsamixer/card_select.c | 201 + alsamixer/card_select.h | 7 + alsamixer/cli.c | 180 + alsamixer/colors.c | 133 + alsamixer/colors.h | 42 + alsamixer/configparser.c | 601 +++ alsamixer/configparser.h | 9 + alsamixer/curskey.c | 274 ++ alsamixer/curskey.h | 37 + alsamixer/device_name.c | 195 + alsamixer/device_name.h | 6 + alsamixer/die.c | 39 + alsamixer/die.h | 7 + alsamixer/go | 10 + alsamixer/mainloop.c | 148 + alsamixer/mainloop.h | 10 + alsamixer/mem.c | 67 + alsamixer/mem.h | 11 + alsamixer/menu_widget.c | 92 + alsamixer/menu_widget.h | 10 + alsamixer/mixer_clickable.c | 136 + alsamixer/mixer_clickable.h | 21 + alsamixer/mixer_controls.c | 519 +++ alsamixer/mixer_controls.h | 37 + alsamixer/mixer_display.c | 763 ++++ alsamixer/mixer_display.h | 10 + alsamixer/mixer_widget.c | 674 ++++ alsamixer/mixer_widget.h | 45 + alsamixer/proc_files.c | 107 + alsamixer/proc_files.h | 6 + alsamixer/textbox.c | 335 ++ alsamixer/textbox.h | 10 + alsamixer/utils.c | 148 + alsamixer/utils.h | 11 + alsamixer/volume_mapping.c | 181 + alsamixer/volume_mapping.h | 19 + alsamixer/widget.c | 139 + alsamixer/widget.h | 33 + alsaucm/Makefile.am | 20 + alsaucm/alsaucm.rst | 235 ++ alsaucm/dump.c | 650 +++ alsaucm/go.sh | 15 + alsaucm/usecase.c | 546 +++ alsaucm/usecase.h | 35 + amidi/Makefile.am | 5 + amidi/amidi.1 | 178 + amidi/amidi.c | 763 ++++ amixer/Makefile.am | 11 + amixer/amixer.1 | 169 + amixer/amixer.c | 1924 +++++++++ amixer/amixer.h | 21 + amixer/go | 10 + amixer/volume_mapping.c | 1 + amixer/volume_mapping.h | 1 + aplay/Makefile.am | 31 + aplay/aplay.1 | 267 ++ aplay/aplay.c | 3564 +++++++++++++++++ aplay/formats.h | 133 + axfer/Makefile.am | 72 + axfer/axfer-list.1 | 86 + axfer/axfer-transfer.1 | 1009 +++++ axfer/axfer.1 | 139 + axfer/container-au.c | 203 + axfer/container-raw.c | 66 + axfer/container-riff-wave.c | 574 +++ axfer/container-voc.c | 835 ++++ axfer/container.c | 459 +++ axfer/container.h | 128 + axfer/frame-cache.c | 113 + axfer/frame-cache.h | 47 + axfer/main.c | 287 ++ axfer/mapper-multiple.c | 259 ++ axfer/mapper-single.c | 191 + axfer/mapper.c | 150 + axfer/mapper.h | 87 + axfer/misc.h | 19 + axfer/subcmd-list.c | 272 ++ axfer/subcmd-transfer.c | 502 +++ axfer/subcmd.h | 18 + axfer/test/Makefile.am | 33 + axfer/test/container-test.c | 324 ++ axfer/test/generator.c | 260 ++ axfer/test/generator.h | 47 + axfer/test/mapper-test.c | 528 +++ axfer/waiter-epoll.c | 107 + axfer/waiter-poll.c | 45 + axfer/waiter-select.c | 109 + axfer/waiter.c | 104 + axfer/waiter.h | 62 + axfer/xfer-libasound-irq-mmap.c | 301 ++ axfer/xfer-libasound-irq-rw.c | 438 ++ axfer/xfer-libasound-timer-mmap.c | 456 +++ axfer/xfer-libasound.c | 934 +++++ axfer/xfer-libasound.h | 92 + axfer/xfer-libffado.c | 570 +++ axfer/xfer-options.c | 628 +++ axfer/xfer.c | 259 ++ axfer/xfer.h | 117 + bat/Makefile.am | 36 + bat/alsa.c | 733 ++++ bat/alsa.h | 20 + bat/alsabat-test.sh | 152 + bat/alsabat.1 | 185 + bat/analyze.c | 514 +++ bat/analyze.h | 16 + bat/bat-signal.h | 31 + bat/bat.c | 747 ++++ bat/common.c | 267 ++ bat/common.h | 269 ++ bat/convert.c | 114 + bat/convert.h | 23 + bat/latencytest.c | 243 ++ bat/latencytest.h | 17 + bat/signal.c | 205 + bat/tests/Makefile.am | 11 + bat/tests/README | 32 + bat/tests/alsabat_main.sh | 178 + .../analog_audio_playback_and_capture.sh | 84 + bat/tests/asound_state/Makefile.am | 6 + bat/tests/asound_state/asound.state.Broadwell | 439 ++ bat/tests/asound_state/asound.state.Haswell | 342 ++ bat/tests/asound_state/asound.state.Skylake | 437 ++ bat/tests/dp_audio_playback.sh | 83 + bat/tests/dp_audio_subdevice_number.sh | 68 + bat/tests/hdmi_audio_playback.sh | 81 + bat/tests/hdmi_audio_subdevice_number.sh | 65 + bat/tests/map_test_case | 8 + bat/tinyalsa.c | 555 +++ bat/tinyalsa.h | 20 + configure-android | 75 + configure.ac | 497 +++ gitcompile | 28 + iecset/Makefile.am | 9 + iecset/iecbits.c | 259 ++ iecset/iecset.1 | 104 + iecset/iecset.c | 433 ++ include/Makefile.am | 25 + include/aconfig.h | 186 + include/bswap.h | 44 + include/gettext.h | 22 + include/gettext_curses.h | 2 + include/os_compat.h | 43 + include/version.h | 9 + nhlt/Makefile.am | 6 + nhlt/nhlt-dmic-info.1 | 37 + nhlt/nhlt-dmic-info.c | 427 ++ po/LINGUAS | 1 + po/Makevars | 78 + po/POTFILES.in | 12 + po/de.po | 1695 ++++++++ po/eu.po | 1716 ++++++++ po/fr.po | 1713 ++++++++ po/ja.po | 1678 ++++++++ po/ka.po | 1764 ++++++++ po/ko.po | 1684 ++++++++ po/sk.po | 1724 ++++++++ seq/Makefile.am | 1 + seq/aconnect/Makefile.am | 7 + seq/aconnect/README.aconnect | 46 + seq/aconnect/aconnect.1 | 148 + seq/aconnect/aconnect.c | 440 ++ seq/aplaymidi/Makefile.am | 5 + seq/aplaymidi/aplaymidi.1 | 69 + seq/aplaymidi/aplaymidi.c | 1045 +++++ seq/aplaymidi/arecordmidi.1 | 82 + seq/aplaymidi/arecordmidi.c | 921 +++++ seq/aplaymidi2/Makefile.am | 5 + seq/aplaymidi2/aplaymidi2.1 | 84 + seq/aplaymidi2/aplaymidi2.c | 581 +++ seq/aplaymidi2/arecordmidi2.1 | 110 + seq/aplaymidi2/arecordmidi2.c | 717 ++++ seq/aseqdump/Makefile.am | 5 + seq/aseqdump/aseqdump.1 | 73 + seq/aseqdump/aseqdump.c | 1233 ++++++ seq/aseqnet/Makefile.am | 7 + seq/aseqnet/README.aseqnet | 55 + seq/aseqnet/aseqnet.1 | 83 + seq/aseqnet/aseqnet.c | 665 +++ seq/aseqsend/Makefile.am | 5 + seq/aseqsend/aseqsend.1 | 77 + seq/aseqsend/aseqsend.c | 458 +++ speaker-test/Makefile.am | 9 + speaker-test/pink.c | 97 + speaker-test/pink.h | 15 + speaker-test/readme.txt | 12 + speaker-test/samples/Front_Center.wav | Bin 0 -> 137134 bytes speaker-test/samples/Front_Left.wav | Bin 0 -> 142128 bytes speaker-test/samples/Front_Right.wav | Bin 0 -> 146990 bytes speaker-test/samples/Makefile.am | 15 + speaker-test/samples/Noise.wav | Bin 0 -> 135202 bytes speaker-test/samples/Rear_Center.wav | Bin 0 -> 130096 bytes speaker-test/samples/Rear_Left.wav | Bin 0 -> 126064 bytes speaker-test/samples/Rear_Right.wav | Bin 0 -> 146480 bytes speaker-test/samples/Side_Left.wav | Bin 0 -> 134868 bytes speaker-test/samples/Side_Right.wav | Bin 0 -> 129966 bytes speaker-test/speaker-test.1 | 189 + speaker-test/speaker-test.c | 1316 ++++++ speaker-test/st2095.c | 164 + speaker-test/st2095.h | 41 + topology/Makefile.am | 25 + topology/alsatplg.rst | 102 + topology/nhlt/Makefile.am | 29 + topology/nhlt/intel/dmic-nhlt.c | 519 +++ topology/nhlt/intel/dmic-nhlt.h | 19 + topology/nhlt/intel/dmic/dmic-debug.c | 188 + topology/nhlt/intel/dmic/dmic-debug.h | 17 + topology/nhlt/intel/dmic/dmic-intel.h | 48 + topology/nhlt/intel/dmic/dmic-internal.h | 335 ++ topology/nhlt/intel/dmic/dmic-process.c | 1378 +++++++ topology/nhlt/intel/dmic/dmic-process.h | 51 + topology/nhlt/intel/dmic/pdm-decim-fir.h | 448 +++ topology/nhlt/intel/intel-nhlt.c | 127 + topology/nhlt/intel/intel-nhlt.h | 51 + topology/nhlt/intel/ssp-nhlt.c | 646 +++ topology/nhlt/intel/ssp-nhlt.h | 19 + topology/nhlt/intel/ssp/ssp-debug.c | 307 ++ topology/nhlt/intel/ssp/ssp-debug.h | 15 + topology/nhlt/intel/ssp/ssp-intel.h | 136 + topology/nhlt/intel/ssp/ssp-internal.h | 371 ++ topology/nhlt/intel/ssp/ssp-process.c | 1341 +++++++ topology/nhlt/intel/ssp/ssp-process.h | 69 + topology/nhlt/nhlt-processor.c | 491 +++ topology/nhlt/nhlt.h | 181 + topology/pre-process-class.c | 301 ++ topology/pre-process-dai.c | 138 + topology/pre-process-dapm.c | 451 +++ topology/pre-process-external.h | 39 + topology/pre-process-object.c | 2094 ++++++++++ topology/pre-processor.c | 1179 ++++++ topology/pre-processor.h | 123 + topology/topology.c | 556 +++ topology/topology.h | 39 + utils/Makefile.am | 4 + utils/alsa-utils.spec.in | 76 + utils/buildrpm | 44 + 300 files changed, 84772 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Android.bp create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 Makefile.am create mode 100644 README.md create mode 100644 TODO create mode 100644 acinclude.m4 create mode 100644 alsa-info/Makefile.am create mode 100755 alsa-info/alsa-info.sh create mode 100644 alsa-info/alsa-info.sh.8 create mode 100644 alsaconf/Makefile.am create mode 100644 alsaconf/alsaconf.8 create mode 100644 alsaconf/alsaconf.fr.8 create mode 100644 alsaconf/alsaconf.in create mode 100644 alsaconf/po/eu.po create mode 100644 alsaconf/po/ja.po create mode 100644 alsaconf/po/ka.po create mode 100644 alsaconf/po/ru.po create mode 100644 alsactl/.gitignore create mode 100644 alsactl/90-alsa-restore.rules.in create mode 100644 alsactl/Makefile.am create mode 100644 alsactl/alsa-restore.service.in create mode 100644 alsactl/alsa-state.service.in create mode 100644 alsactl/alsactl.1 create mode 100644 alsactl/alsactl.c create mode 100644 alsactl/alsactl.h create mode 100644 alsactl/alsactl_init.xml create mode 100644 alsactl/clean.c create mode 100644 alsactl/daemon.c create mode 100644 alsactl/info.c create mode 100644 alsactl/init/00main create mode 100644 alsactl/init/Makefile.am create mode 100644 alsactl/init/ca0106 create mode 100644 alsactl/init/default create mode 100644 alsactl/init/hda create mode 100644 alsactl/init/help create mode 100644 alsactl/init/info create mode 100644 alsactl/init/test create mode 100644 alsactl/init_parse.c create mode 100644 alsactl/init_sysdeps.c create mode 100644 alsactl/init_sysfs.c create mode 100644 alsactl/init_ucm.c create mode 100644 alsactl/init_utils_run.c create mode 100644 alsactl/init_utils_string.c create mode 100644 alsactl/list.h create mode 100644 alsactl/lock.c create mode 100644 alsactl/monitor.c create mode 100644 alsactl/state.c create mode 100644 alsactl/utils.c create mode 100644 alsaloop/Makefile.am create mode 100644 alsaloop/alsaloop.1 create mode 100644 alsaloop/alsaloop.c create mode 100644 alsaloop/alsaloop.h create mode 100644 alsaloop/control.c create mode 100644 alsaloop/effect-sweep.c create mode 100644 alsaloop/pcmjob.c create mode 100755 alsaloop/test.sh create mode 100644 alsamixer/Makefile.am create mode 100644 alsamixer/alsamixer.1 create mode 100644 alsamixer/alsamixer.rc.example create mode 100644 alsamixer/bindings.c create mode 100644 alsamixer/bindings.h create mode 100644 alsamixer/card_select.c create mode 100644 alsamixer/card_select.h create mode 100644 alsamixer/cli.c create mode 100644 alsamixer/colors.c create mode 100644 alsamixer/colors.h create mode 100644 alsamixer/configparser.c create mode 100644 alsamixer/configparser.h create mode 100644 alsamixer/curskey.c create mode 100644 alsamixer/curskey.h create mode 100644 alsamixer/device_name.c create mode 100644 alsamixer/device_name.h create mode 100644 alsamixer/die.c create mode 100644 alsamixer/die.h create mode 100755 alsamixer/go create mode 100644 alsamixer/mainloop.c create mode 100644 alsamixer/mainloop.h create mode 100644 alsamixer/mem.c create mode 100644 alsamixer/mem.h create mode 100644 alsamixer/menu_widget.c create mode 100644 alsamixer/menu_widget.h create mode 100644 alsamixer/mixer_clickable.c create mode 100644 alsamixer/mixer_clickable.h create mode 100644 alsamixer/mixer_controls.c create mode 100644 alsamixer/mixer_controls.h create mode 100644 alsamixer/mixer_display.c create mode 100644 alsamixer/mixer_display.h create mode 100644 alsamixer/mixer_widget.c create mode 100644 alsamixer/mixer_widget.h create mode 100644 alsamixer/proc_files.c create mode 100644 alsamixer/proc_files.h create mode 100644 alsamixer/textbox.c create mode 100644 alsamixer/textbox.h create mode 100644 alsamixer/utils.c create mode 100644 alsamixer/utils.h create mode 100644 alsamixer/volume_mapping.c create mode 100644 alsamixer/volume_mapping.h create mode 100644 alsamixer/widget.c create mode 100644 alsamixer/widget.h create mode 100644 alsaucm/Makefile.am create mode 100644 alsaucm/alsaucm.rst create mode 100644 alsaucm/dump.c create mode 100755 alsaucm/go.sh create mode 100644 alsaucm/usecase.c create mode 100644 alsaucm/usecase.h create mode 100644 amidi/Makefile.am create mode 100644 amidi/amidi.1 create mode 100644 amidi/amidi.c create mode 100644 amixer/Makefile.am create mode 100644 amixer/amixer.1 create mode 100644 amixer/amixer.c create mode 100644 amixer/amixer.h create mode 100755 amixer/go create mode 120000 amixer/volume_mapping.c create mode 120000 amixer/volume_mapping.h create mode 100644 aplay/Makefile.am create mode 100644 aplay/aplay.1 create mode 100644 aplay/aplay.c create mode 100644 aplay/formats.h create mode 100644 axfer/Makefile.am create mode 100644 axfer/axfer-list.1 create mode 100644 axfer/axfer-transfer.1 create mode 100644 axfer/axfer.1 create mode 100644 axfer/container-au.c create mode 100644 axfer/container-raw.c create mode 100644 axfer/container-riff-wave.c create mode 100644 axfer/container-voc.c create mode 100644 axfer/container.c create mode 100644 axfer/container.h create mode 100644 axfer/frame-cache.c create mode 100644 axfer/frame-cache.h create mode 100644 axfer/main.c create mode 100644 axfer/mapper-multiple.c create mode 100644 axfer/mapper-single.c create mode 100644 axfer/mapper.c create mode 100644 axfer/mapper.h create mode 100644 axfer/misc.h create mode 100644 axfer/subcmd-list.c create mode 100644 axfer/subcmd-transfer.c create mode 100644 axfer/subcmd.h create mode 100644 axfer/test/Makefile.am create mode 100644 axfer/test/container-test.c create mode 100644 axfer/test/generator.c create mode 100644 axfer/test/generator.h create mode 100644 axfer/test/mapper-test.c create mode 100644 axfer/waiter-epoll.c create mode 100644 axfer/waiter-poll.c create mode 100644 axfer/waiter-select.c create mode 100644 axfer/waiter.c create mode 100644 axfer/waiter.h create mode 100644 axfer/xfer-libasound-irq-mmap.c create mode 100644 axfer/xfer-libasound-irq-rw.c create mode 100644 axfer/xfer-libasound-timer-mmap.c create mode 100644 axfer/xfer-libasound.c create mode 100644 axfer/xfer-libasound.h create mode 100644 axfer/xfer-libffado.c create mode 100644 axfer/xfer-options.c create mode 100644 axfer/xfer.c create mode 100644 axfer/xfer.h create mode 100644 bat/Makefile.am create mode 100644 bat/alsa.c create mode 100644 bat/alsa.h create mode 100755 bat/alsabat-test.sh create mode 100644 bat/alsabat.1 create mode 100644 bat/analyze.c create mode 100644 bat/analyze.h create mode 100644 bat/bat-signal.h create mode 100644 bat/bat.c create mode 100644 bat/common.c create mode 100644 bat/common.h create mode 100644 bat/convert.c create mode 100644 bat/convert.h create mode 100644 bat/latencytest.c create mode 100644 bat/latencytest.h create mode 100644 bat/signal.c create mode 100644 bat/tests/Makefile.am create mode 100644 bat/tests/README create mode 100755 bat/tests/alsabat_main.sh create mode 100755 bat/tests/analog_audio_playback_and_capture.sh create mode 100644 bat/tests/asound_state/Makefile.am create mode 100644 bat/tests/asound_state/asound.state.Broadwell create mode 100644 bat/tests/asound_state/asound.state.Haswell create mode 100644 bat/tests/asound_state/asound.state.Skylake create mode 100755 bat/tests/dp_audio_playback.sh create mode 100755 bat/tests/dp_audio_subdevice_number.sh create mode 100755 bat/tests/hdmi_audio_playback.sh create mode 100755 bat/tests/hdmi_audio_subdevice_number.sh create mode 100644 bat/tests/map_test_case create mode 100644 bat/tinyalsa.c create mode 100644 bat/tinyalsa.h create mode 100755 configure-android create mode 100644 configure.ac create mode 100755 gitcompile create mode 100644 iecset/Makefile.am create mode 100644 iecset/iecbits.c create mode 100644 iecset/iecset.1 create mode 100644 iecset/iecset.c create mode 100644 include/Makefile.am create mode 100644 include/aconfig.h create mode 100644 include/bswap.h create mode 100644 include/gettext.h create mode 100644 include/gettext_curses.h create mode 100644 include/os_compat.h create mode 100644 include/version.h create mode 100644 nhlt/Makefile.am create mode 100644 nhlt/nhlt-dmic-info.1 create mode 100644 nhlt/nhlt-dmic-info.c create mode 100644 po/LINGUAS create mode 100644 po/Makevars create mode 100644 po/POTFILES.in create mode 100644 po/de.po create mode 100644 po/eu.po create mode 100644 po/fr.po create mode 100644 po/ja.po create mode 100644 po/ka.po create mode 100644 po/ko.po create mode 100644 po/sk.po create mode 100644 seq/Makefile.am create mode 100644 seq/aconnect/Makefile.am create mode 100644 seq/aconnect/README.aconnect create mode 100644 seq/aconnect/aconnect.1 create mode 100644 seq/aconnect/aconnect.c create mode 100644 seq/aplaymidi/Makefile.am create mode 100644 seq/aplaymidi/aplaymidi.1 create mode 100644 seq/aplaymidi/aplaymidi.c create mode 100644 seq/aplaymidi/arecordmidi.1 create mode 100644 seq/aplaymidi/arecordmidi.c create mode 100644 seq/aplaymidi2/Makefile.am create mode 100644 seq/aplaymidi2/aplaymidi2.1 create mode 100644 seq/aplaymidi2/aplaymidi2.c create mode 100644 seq/aplaymidi2/arecordmidi2.1 create mode 100644 seq/aplaymidi2/arecordmidi2.c create mode 100644 seq/aseqdump/Makefile.am create mode 100644 seq/aseqdump/aseqdump.1 create mode 100644 seq/aseqdump/aseqdump.c create mode 100644 seq/aseqnet/Makefile.am create mode 100644 seq/aseqnet/README.aseqnet create mode 100644 seq/aseqnet/aseqnet.1 create mode 100644 seq/aseqnet/aseqnet.c create mode 100644 seq/aseqsend/Makefile.am create mode 100644 seq/aseqsend/aseqsend.1 create mode 100644 seq/aseqsend/aseqsend.c create mode 100644 speaker-test/Makefile.am create mode 100644 speaker-test/pink.c create mode 100644 speaker-test/pink.h create mode 100644 speaker-test/readme.txt create mode 100644 speaker-test/samples/Front_Center.wav create mode 100644 speaker-test/samples/Front_Left.wav create mode 100644 speaker-test/samples/Front_Right.wav create mode 100644 speaker-test/samples/Makefile.am create mode 100644 speaker-test/samples/Noise.wav create mode 100644 speaker-test/samples/Rear_Center.wav create mode 100644 speaker-test/samples/Rear_Left.wav create mode 100644 speaker-test/samples/Rear_Right.wav create mode 100644 speaker-test/samples/Side_Left.wav create mode 100644 speaker-test/samples/Side_Right.wav create mode 100644 speaker-test/speaker-test.1 create mode 100644 speaker-test/speaker-test.c create mode 100644 speaker-test/st2095.c create mode 100644 speaker-test/st2095.h create mode 100644 topology/Makefile.am create mode 100644 topology/alsatplg.rst create mode 100644 topology/nhlt/Makefile.am create mode 100644 topology/nhlt/intel/dmic-nhlt.c create mode 100644 topology/nhlt/intel/dmic-nhlt.h create mode 100644 topology/nhlt/intel/dmic/dmic-debug.c create mode 100644 topology/nhlt/intel/dmic/dmic-debug.h create mode 100644 topology/nhlt/intel/dmic/dmic-intel.h create mode 100644 topology/nhlt/intel/dmic/dmic-internal.h create mode 100644 topology/nhlt/intel/dmic/dmic-process.c create mode 100644 topology/nhlt/intel/dmic/dmic-process.h create mode 100644 topology/nhlt/intel/dmic/pdm-decim-fir.h create mode 100644 topology/nhlt/intel/intel-nhlt.c create mode 100644 topology/nhlt/intel/intel-nhlt.h create mode 100644 topology/nhlt/intel/ssp-nhlt.c create mode 100644 topology/nhlt/intel/ssp-nhlt.h create mode 100644 topology/nhlt/intel/ssp/ssp-debug.c create mode 100644 topology/nhlt/intel/ssp/ssp-debug.h create mode 100644 topology/nhlt/intel/ssp/ssp-intel.h create mode 100644 topology/nhlt/intel/ssp/ssp-internal.h create mode 100644 topology/nhlt/intel/ssp/ssp-process.c create mode 100644 topology/nhlt/intel/ssp/ssp-process.h create mode 100644 topology/nhlt/nhlt-processor.c create mode 100644 topology/nhlt/nhlt.h create mode 100644 topology/pre-process-class.c create mode 100644 topology/pre-process-dai.c create mode 100644 topology/pre-process-dapm.c create mode 100644 topology/pre-process-external.h create mode 100644 topology/pre-process-object.c create mode 100644 topology/pre-processor.c create mode 100644 topology/pre-processor.h create mode 100644 topology/topology.c create mode 100644 topology/topology.h create mode 100644 utils/Makefile.am create mode 100644 utils/alsa-utils.spec.in create mode 100644 utils/buildrpm diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..71af32f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,171 @@ +name: Build alsa-utils + +on: [push, pull_request] + +jobs: + fedora_latest_build: + runs-on: ubuntu-latest + container: + image: fedora:latest + steps: + - name: Prepare environment + run: | + dnf -y upgrade + dnf -y install @development-tools libtool bzip2 gettext-devel ncurses-devel + + - name: Checkout alsa-lib + uses: actions/checkout@v4 + with: + repository: alsa-project/alsa-lib + ref: master + path: alsa-lib + - name: Configure alsa-lib + run: | + cd alsa-lib + head -5 configure.ac + libtoolize --force --copy --automake + aclocal + autoheader + automake --foreign --copy --add-missing + autoconf + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + echo "Version: $(cat version)" + - name: Build alsa-lib + run: | + cd alsa-lib + make + - name: Install alsa-lib + run: | + cd alsa-lib + make install + + - name: Checkout + uses: actions/checkout@v4 + with: + path: alsa-utils + - name: Checkout all tags + run: | + cd alsa-utils + git fetch --prune --tags --force + git fetch --prune --unshallow --force + - name: Modify version + run: | + cd alsa-utils + mv configure.ac configure.ac.old + version=$(git describe | sed -e 's/v//') + if test -z "$version"; then version=$(git describe --tags | sed -e 's/v//'); fi + if test -z "$version"; then version1=$(cat configure.ac.old | grep "AC_INIT(.*)" | cut -d ' ' -f 2 | sed -e 's/)//'); version2=$(git rev-parse --short HEAD); version="${version1}-g${version2}"; fi + echo "Version: ${version}" + sed -r "s/AC_INIT\(alsa-utils,.*\)/AC_INIT(alsa-utils, ${version})/" < configure.ac.old > configure.ac.tmp + sed -e 's;po/Makefile.in;;' < configure.ac.tmp > configure.ac + grep AC_INIT configure.ac + - name: Configure + run: | + cd alsa-utils + head -5 configure.ac + aclocal + gettextize -c -f --no-changelog + libtoolize --force --copy --automake + aclocal -I m4 + autoheader + automake --foreign --copy --add-missing + autoconf + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + echo "Version: $(cat version)" + - name: Build + run: | + cd alsa-utils + make + - name: Install test + run: | + cd alsa-utils + make install + - name: Create package + run: | + cd alsa-utils + make dist-bzip2 + - name: Unpack package + run: | + mkdir artifacts + cd alsa-utils + tar xjf alsa-utils-$(cat version).tar.bz2 + cp alsa-utils-$(cat version).tar.bz2 ../artifacts + - name: Configure package + run: | + cd alsa-utils + cd alsa-utils-$(cat version) + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + - name: Build package + run: | + cd alsa-utils + cd alsa-utils-$(cat version) + make + - name: Install package + run: | + cd alsa-utils + cd alsa-utils-$(cat version) + make install + - name: Archive package + uses: actions/upload-artifact@v4 + with: + name: alsa-utils-test-package + path: artifacts/ + + ubuntu_last_build: + runs-on: ubuntu-latest + container: + image: ubuntu:latest + steps: + - name: Prepare + run: | + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get -y install apt-utils + apt-get -y full-upgrade + apt-get install -y git build-essential pkg-config m4 autoconf automake libtool gettext ncurses-dev + + - name: Checkout alsa-lib + uses: actions/checkout@v4 + with: + repository: alsa-project/alsa-lib + ref: master + - name: Configure alsa-lib + run: | + libtoolize --force --copy --automake + aclocal + autoheader + automake --foreign --copy --add-missing + autoconf + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + - name: Build alsa-lib + run: | + make + - name: Install alsa-lib + run: | + make install + + - name: Checkout + uses: actions/checkout@v4 + - name: Configure + run: | + mv configure.ac configure.ac.old + sed -e 's;po/Makefile.in;;' < configure.ac.old > configure.ac + aclocal + gettextize -c -f --no-changelog + libtoolize --force --copy --automake + aclocal -I m4 + autoheader + automake --foreign --copy --add-missing + autoconf + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + - name: Build + run: | + make + - name: Install + run: | + make install diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..235e36a --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +configure +config.log +config.cache +config.status +config.guess +config.rpath +config.sub +Makefile.conf +confdefs.h +aclocal.m4 +Makefile +Makefile.in +libtool +ltmain.sh +ltconfig +version +autom4te.cache +compile +depcomp +missing +install-sh +mkinstalldirs +test-driver +ABOUT-NLS +*.ok +*.gmo +*.la +*.lo +*.o +*~ +.deps +.libs +.dirstamp + +alsactl/alsactl +alsactl/alsactl_init.7 +alsactl/alsa-state.service +alsactl/alsa-restore.service +alsactl/alsa-store.service +alsaconf/alsaconf +alsamixer/alsamixer +amidi/amidi +amixer/amixer +aplay/aplay +aplay/arecord +aplay/arecord.1 +axfer/axfer +bat/alsabat +iecset/iecset +seq/aconnect/aconnect +seq/aplaymidi/aplaymidi +seq/aplaymidi/arecordmidi +seq/aplaymidi2/aplaymidi2 +seq/aplaymidi2/arecordmidi2 +seq/aseqdump/aseqdump +seq/aseqsend/aseqsend +seq/aseqnet/aseqnet +speaker-test/speaker-test +topology/alsatplg.1 +alsaloop/alsaloop +alsaucm/alsaucm +alsaucm/alsaucm.1 +alsaucm/89-alsa-ucm.rules +topology/alsatplg +nhlt/nhlt-dmic-info + +include/aconfig.h.in +include/stamp-* +po/Makefile* +po/POTFILES +po/Rules-quot +po/*.header +po/*.sin +po/*.sed +po/*.gmo +po/stamp-po +po/alsa-utils.pot +po/Makevars.template +po/ChangeLog +m4/* +utils/*.spec diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..aa44652 --- /dev/null +++ b/Android.bp @@ -0,0 +1,140 @@ +// +// Copyright (C) 2025 KonstaKANG +// +// SPDX-License-Identifier: GPL-2.0-or-later +// + +cc_defaults { + name: "alsa-utils_defaults", + vendor: true, + local_include_dirs: [ + "include", + ], + shared_libs: [ + "libasound", + ], + cflags: [ + "-O2", + "-DPIC", + "-fPIC", + ], +} + +cc_binary { + name: "alsactl", + srcs: [ + "alsactl/alsactl.c", + "alsactl/clean.c", + "alsactl/daemon.c", + "alsactl/info.c", + "alsactl/init_parse.c", + "alsactl/init_ucm.c", + "alsactl/lock.c", + "alsactl/monitor.c", + "alsactl/state.c", + "alsactl/utils.c", + ], + cflags: [ + "-DSYS_ASOUNDRC=\"/data/local/tmp/asound.state\"", + "-DSYS_LOCKFILE=\"asound.state.lock\"", + "-DSYS_LOCKPATH=\"/data/local/tmp/\"", + "-DSYS_PIDFILE=\"/data/local/tmp/alsactl.pid\"", + "-D_GNU_SOURCE", + "-D__USE_GNU", + "-Wno-parentheses", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "alsaloop", + srcs: [ + "alsaloop/alsaloop.c", + "alsaloop/control.c", + "alsaloop/pcmjob.c", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "alsaucm", + srcs: [ + "alsaucm/dump.c", + "alsaucm/usecase.c", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "alsamidi", + srcs: [ + "amidi/amidi.c", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "amixer", + srcs: [ + "amixer/amixer.c", + "amixer/volume_mapping.c", + ], + cflags: [ + "-D_GNU_SOURCE", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "aplay", + srcs: [ + "aplay/aplay.c", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "arecord", + srcs: [ + "aplay/aplay.c", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "axfer", + srcs: [ + "axfer/container-au.c", + "axfer/container-raw.c", + "axfer/container-riff-wave.c", + "axfer/container-voc.c", + "axfer/container.c", + "axfer/frame-cache.c", + "axfer/main.c", + "axfer/mapper-multiple.c", + "axfer/mapper-single.c", + "axfer/mapper.c", + "axfer/subcmd-list.c", + "axfer/subcmd-transfer.c", + "axfer/waiter-epoll.c", + "axfer/waiter-poll.c", + "axfer/waiter-select.c", + "axfer/waiter.c", + "axfer/xfer-libasound-irq-mmap.c", + "axfer/xfer-libasound-irq-rw.c", + "axfer/xfer-libasound-timer-mmap.c", + "axfer/xfer-libasound.c", + "axfer/xfer-options.c", + "axfer/xfer.c", + ], + defaults: ["alsa-utils_defaults"], +} + +cc_binary { + name: "iecset", + srcs: [ + "iecset/iecbits.c", + "iecset/iecset.c", + ], + defaults: ["alsa-utils_defaults"], +} diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..623b625 --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..06849ec --- /dev/null +++ b/ChangeLog @@ -0,0 +1,43 @@ +2005-08-13 gettextize + + * Makefile.am (EXTRA_DIST): Add config.rpath, mkinstalldirs. + * configure.in (AM_GNU_GETTEXT_VERSION): Bump to 0.14.4. + +0.0.7 -> 0.2.0 + +* removed error message from alsamixer +* fixes in alsactl program +* version is now compatible with driver + +0.0.6 -> 0.0.7 + +* added alsactl control program +* updated alsamixer by Carl van Schaik + - stereo record source select + - left/right route support + +0.0.5 -> 0.0.6 + +* updates for new alsa-lib +* fixed compilation problems with config.h header file +* updated output 'aplay -l' + +0.0.3 -> 0.0.4 + +* added check for alsa-lib package +* added spec file for RPM + +0.0.2 -> 0.0.3 + +* included great ncurses alsamixer from Tim Janik +* enhanced Mixer from Andy Lo A Foe renamed to amixer + +0.0.1 -> 0.0.2 + +* fixed small bug in aplay in command line parsing +* fixed fragment size allocation for low rate files in aplay +* a little bit corrected output from Mixer + +0.0.1 + +* initial release (aplay, broken Mixer) diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..5f20814 --- /dev/null +++ b/INSTALL @@ -0,0 +1,56 @@ + + ALSA utils installation + ======================= + +Requirements +------------ + +alsamixer requires the ncurses library. Some systems need to install +the develop package additionally to link with this library. + +alsaconf requires dialog or whiptail program to run properly. + +If systemd (minimum version 18) is installed it will be used to run +alsactl to store and restore settings. + +Installation +------------ + +For installation you can use these commands: + + ./configure + make install + +If ./configure command complain that alsa-lib package isn't installed, +please, check if --prefix option is same for alsa-lib and alsa-utils +package. The configure script from alsa-utils package probably cannot find +header file asoundlib.h in $prefix/include/alsa directory (usually in +/usr/include/alsa directory). + +Compilation from git sources +--------------------------- + +You need also GNU packages autoconf and automake installed in your system +to compile git sources of alsa-utils package. + +For compilation you can use these commands: + + aclocal + gettextize + autoheader + automake --foreign --copy --add-missing + autoconf + ./configure + make + +The included gitcompile script does this job for you. + +To configure the build to install in a local directory, a command like the +following can be used: + + ./gitcompile --prefix="$PWD/build" \ + --with-systemdsystemunitdir="$PWD/build/$(pkg-config systemd --variable=systemdsystemunitdir)" \ + --with-udev-rules-dir="$PWD/build/$(pkg-config udev --variable=udevdir)" + +Note: Some automake packages have missing aclocal program. Use newer version + in the case. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..b961506 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,57 @@ +AM_CPPFLAGS=-I$(top_srcdir)/include + +SUBDIRS = include alsactl utils m4 po alsa-info +if ALSAMIXER +SUBDIRS += alsamixer +endif +if HAVE_MIXER +SUBDIRS += amixer +endif +if HAVE_RAWMIDI +SUBDIRS += amidi +endif +if ALSACONF +SUBDIRS += alsaconf +endif +if HAVE_PCM +SUBDIRS += aplay iecset speaker-test axfer +if ALSALOOP +SUBDIRS += alsaloop +endif +if BAT +SUBDIRS += bat +endif +endif +if HAVE_SEQ +SUBDIRS += seq +endif +if HAVE_UCM +SUBDIRS += alsaucm +endif +if HAVE_TOPOLOGY +SUBDIRS += topology +endif +if NHLT +SUBDIRS += nhlt +endif + +EXTRA_DIST= README.md TODO gitcompile +AUTOMAKE_OPTIONS=foreign +ACLOCAL_AMFLAGS = -I m4 + +rpm: dist + $(MAKE) -C $@ + +dist-hook: + -chmod -R a+r $(distdir) + @if ! test -z "$(AMTAR)"; then \ + $(AMTAR) --create --verbose --file=- $(distdir) | bzip2 -c -9 > $(distdir).tar.bz2 ; \ + else \ + $(TAR) --create --verbose --file=- $(distdir) | bzip2 -c -9 > $(distdir).tar.bz2 ; \ + fi + +install-data-hook: + $(MKDIR_P) -m 0755 $(DESTDIR)$(ASOUND_STATE_DIR) + +DISTCHECK_CONFIGURE_FLAGS = \ + --with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir) diff --git a/README.md b/README.md new file mode 100644 index 0000000..50db0a7 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# alsa-utils +## Advanced Linux Sound Architecture - Utilities + +[![Build alsa-utils](https://github.com/alsa-project/alsa-utils/workflows/Build%20alsa-utils/badge.svg?branch=master)](https://github.com/alsa-project/alsa-utils/actions/workflows/build.yml?query=branch%3Amaster++) + +This package contains the command line utilities for the ALSA project. +The package can be compiled only with the installed ALSA driver and +the ALSA C library (alsa-lib). + +Utility | Description +----------------|---------------------------------------------------- +alsaconf | the ALSA driver configurator script +alsa-info | a script to gather information about ALSA subsystem +alsactl | an utility for soundcard settings management +aplay/arecord | an utility for the playback / capture of .wav,.voc,.au files +axfer | an utility to transfer audio data frame (enhancement of aplay) +amixer | a command line mixer +alsamixer | a ncurses mixer +amidi | a utility to send/receive sysex dumps or other MIDI data +iecset | a utility to show/set the IEC958 status bits +speaker-test | a speaker test utility +alsaloop | a software loopback for PCM devices +alsaucm | Use Case Manager utility +alsabat | a sound tester for ALSA sound card driver +alsatplg | ALSA topology compiler + +You may give a look for more information about the ALSA project to URL +http://www.alsa-project.org. diff --git a/TODO b/TODO new file mode 100644 index 0000000..a1d880f --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +H add options to aplay to avoid xrun using arecord | aplay +M Rewrite aplay/arecord tool and separate the experimental stuff +M Write a *good* mixer +L Add support for OSS setups to alsactl diff --git a/acinclude.m4 b/acinclude.m4 new file mode 100644 index 0000000..49550f1 --- /dev/null +++ b/acinclude.m4 @@ -0,0 +1,12 @@ +AC_DEFUN([SAVE_UTIL_VERSION], [ +SND_UTIL_VERSION=$VERSION +echo $VERSION > $srcdir/version +AC_DEFINE_UNQUOTED(VERSION, "$SND_UTIL_VERSION", [ALSA util version]) +AC_SUBST(SND_UTIL_VERSION) +SND_UTIL_MAJOR=`echo $VERSION | cut -d . -f 1` +AC_SUBST(SND_UTIL_MAJOR) +SND_UTIL_MINOR=`echo $VERSION | cut -d . -f 2` +AC_SUBST(SND_UTIL_MINOR) +SND_UTIL_SUBMINOR=`echo $VERSION | cut -d . -f 3 | sed -e 's/pre[[0-9]]*//g'` +AC_SUBST(SND_UTIL_SUBMINOR) +]) diff --git a/alsa-info/Makefile.am b/alsa-info/Makefile.am new file mode 100644 index 0000000..92679b3 --- /dev/null +++ b/alsa-info/Makefile.am @@ -0,0 +1,3 @@ +EXTRA_DIST = alsa-info.sh alsa-info.sh.8 +sbin_SCRIPTS = alsa-info.sh +man_MANS = alsa-info.sh.8 diff --git a/alsa-info/alsa-info.sh b/alsa-info/alsa-info.sh new file mode 100755 index 0000000..77ca624 --- /dev/null +++ b/alsa-info/alsa-info.sh @@ -0,0 +1,1019 @@ +#!/bin/bash + +SCRIPT_VERSION=0.5.3 +CHANGELOG='https://www.alsa-project.org/alsa-info.sh.changelog' + +################################################################################# +#Copyright (C) 2007 Free Software Foundation. + +#This program is free software; you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation; either version 2 of the License, or +#(at your option) any later version. + +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. + +#You should have received a copy of the GNU General Public License +#along with this program; if not, write to the Free Software +#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +################################################################################## + +# The script was written for 2 main reasons: +# 1. Remove the need for the devs/helpers to ask several questions before we can easily help the user. +# 2. Allow newer/inexperienced ALSA users to give us all the info we need to help them. + +#Set the locale (this may or may not be a good idea.. let me know) +export LC_ALL=C + +# Change the PATH variable, so we can run lspci (needed for some distros) +PATH="$PATH:/bin:/sbin:/usr/bin:/usr/sbin" +BGTITLE="ALSA-Info v $SCRIPT_VERSION" +PASTEBINKEY='C9cRIO8m/9y8Cs0nVs0FraRx7U0pHsuc' + +WGET="$(command -v wget)" +REQUIRES=(mktemp grep pgrep awk date uname cat sort dmesg amixer alsactl) + +# +# Define some simple functions +# + +update() { + test -z "$WGET" || test ! -x "$WGET" && return + + SHFILE=$(mktemp -t alsa-info.XXXXXXXXXX) || exit 1 + wget -O $SHFILE "https://www.alsa-project.org/alsa-info.sh" >/dev/null 2>&1 + REMOTE_VERSION=$(grep SCRIPT_VERSION $SHFILE | head -n1 | sed 's/.*=//') + if [ -s "$SHFILE" ] && [ "$REMOTE_VERSION" != "$SCRIPT_VERSION" ]; then + if [[ -n $DIALOG ]] + then + OVERWRITE= + if [ -w $0 ]; then + dialog --yesno "Newer version of ALSA-Info has been found\n\nDo you wish to install it?\nNOTICE: The original file $0 will be overwritten!" 0 0 + DIALOG_EXIT_CODE=$? + if [[ $DIALOG_EXIT_CODE = 0 ]]; then + OVERWRITE=yes + fi + fi + if [ -z "$OVERWRITE" ]; then + dialog --yesno "Newer version of ALSA-Info has been found\n\nDo you wish to download it?" 0 0 + DIALOG_EXIT_CODE=$? + fi + if [[ $DIALOG_EXIT_CODE = 0 ]] + then + echo "Newer version detected: $REMOTE_VERSION" + echo "To view the ChangeLog, please visit $CHANGELOG" + if [ "$OVERWRITE" = "yes" ]; then + cp $SHFILE $0 + echo "ALSA-Info script has been updated to v $REMOTE_VERSION" + echo "Please re-run the script" + rm $SHFILE 2>/dev/null + else + echo "ALSA-Info script has been downloaded as $SHFILE." + echo "Please re-run the script from new location." + fi + exit + else + rm $SHFILE 2>/dev/null + fi + else + echo "Newer version detected: $REMOTE_VERSION" + echo "To view the ChangeLog, please visit $CHANGELOG" + if [ -w $0 ]; then + echo "The original file $0 will be overwritten!" + echo -n "If you do not like to proceed, press Ctrl-C now.." ; read inp + cp $SHFILE $0 + echo "ALSA-Info script has been updated. Please re-run it." + rm $SHFILE 2>/dev/null + else + echo "ALSA-Info script has been downloaded $SHFILE." + echo "Please, re-run it from new location." + fi + exit + fi + else + rm $SHFILE 2>/dev/null + fi +} + +cleanup() { + if [ -n "$TEMPDIR" ] && [ "$KEEP_FILES" != "yes" ]; then + rm -rf "$TEMPDIR" 2>/dev/null + fi + test -n "$KEEP_OUTPUT" || rm -f "$NFILE" +} + + +withaplay() { + echo "!!Aplay/Arecord output" >> "$FILE" + echo "!!--------------------" >> "$FILE" + echo "" >> "$FILE" + echo "APLAY" >> "$FILE" + echo "" >> "$FILE" + aplay -l >> "$FILE" 2>&1 + echo "" >> "$FILE" + echo "ARECORD" >> "$FILE" + echo "" >> "$FILE" + arecord -l >> "$FILE" 2>&1 + echo "" >> "$FILE" +} + +withmodules() { + echo "!!All Loaded Modules" >> "$FILE" + echo "!!------------------" >> "$FILE" + echo "" >> "$FILE" + awk '{print $1}' < /proc/modules | sort >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" +} + +withamixer() { + echo "!!Amixer output" >> "$FILE" + echo "!!-------------" >> "$FILE" + echo "" >> "$FILE" + for f in /proc/asound/card*/id; do + [ -f "$f" ] && read -r CARD_NAME < "$f" || continue + echo "!!-------Mixer controls for card $CARD_NAME" >> "$FILE" + echo "" >> "$FILE" + amixer -c "$CARD_NAME" info >> "$FILE" 2>&1 + amixer -c "$CARD_NAME" >> "$FILE" 2>&1 + echo "" >> "$FILE" + done + echo "" >> "$FILE" +} + +withalsactl() { + echo "!!Alsactl output" >> "$FILE" + echo "!!--------------" >> "$FILE" + echo "" >> "$FILE" + alsactl -f "$TEMPDIR/alsactl.tmp" store + echo "--startcollapse--" >> "$FILE" + cat "$TEMPDIR/alsactl.tmp" >> "$FILE" + echo "--endcollapse--" >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" +} + +withdevices() { + echo "!!ALSA Device nodes" >> "$FILE" + echo "!!-----------------" >> "$FILE" + echo "" >> "$FILE" + ls -la /dev/snd/* >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" +} + +withconfigs() { +if [[ -e "$HOME/.asoundrc" ]] || [[ -e "/etc/asound.conf" ]] || [[ -e "$HOME/.asoundrc.asoundconf" ]]; then + echo "!!ALSA configuration files" >> "$FILE" + echo "!!------------------------" >> "$FILE" + echo "" >> "$FILE" + + #Check for ~/.asoundrc + if [[ -e "$HOME/.asoundrc" ]] + then + echo "!!User specific config file (~/.asoundrc)" >> "$FILE" + echo "" >> "$FILE" + cat "$HOME/.asoundrc" >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" + fi + #Check for .asoundrc.asoundconf (seems to be Ubuntu specific) + if [[ -e "$HOME/.asoundrc.asoundconf" ]] + then + echo "!!asoundconf-generated config file" >> "$FILE" + echo "" >> "$FILE" + cat "$HOME/.asoundrc.asoundconf" >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" + fi + #Check for /etc/asound.conf + if [[ -e /etc/asound.conf ]] + then + echo "!!System wide config file (/etc/asound.conf)" >> "$FILE" + echo "" >> "$FILE" + cat /etc/asound.conf >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" + fi +fi +} + +withsysfs() { + local i f + local printed="" + for i in /sys/class/sound/*; do + case "$i" in + */hwC?D?) + if [ -f "$i/init_pin_configs" ]; then + if [ -z "$printed" ]; then + echo "!!Sysfs Files" >> "$FILE" + echo "!!-----------" >> "$FILE" + echo "" >> "$FILE" + fi + for f in init_pin_configs driver_pin_configs user_pin_configs init_verbs hints; do + echo "$i/$f:" >> "$FILE" + cat "$i/$f" >> "$FILE" + echo >> "$FILE" + done + printed=yes + fi + ;; + esac + done + if [ -n "$printed" ]; then + echo "" >> "$FILE" + fi +} + +withdmesg() { + echo "!!ALSA/HDA dmesg" >> "$FILE" + echo "!!--------------" >> "$FILE" + echo "" >> "$FILE" + dmesg | grep -C1 -E 'ALSA|HDA|HDMI|snd[_-]|sound|audio|hda.codec|hda.intel' >> "$FILE" + echo "" >> "$FILE" + echo "" >> "$FILE" +} + +withpackages() { + local RPM + local DPKG + RPM="$(command -v rpmquery)" + DPKG="$(command -v dpkg)" + [ -n "$RPM$DPKG" ] || return + local PATTERN='(alsa-(lib|oss|plugins|tools|ucm|(topology|ucm)-conf|utils|sof-firmware)|libalsa|tinycompress|sof-firmware)' + { + echo "!!Packages installed" + echo "!!--------------------" + echo "" + { + if [ -x "$RPM" ]; then "$RPM" -a; fi + if [ -x "$DPKG" ]; then "$DPKG" -l; fi + } | grep -E "$PATTERN" + echo "" + } >> "$FILE" +} + +withall() { + withdevices + withconfigs + withaplay + withamixer + withalsactl + withmodules + withsysfs + withdmesg + withpackages + WITHALL=no +} + +get_alsa_library_version() { + ALSA_LIB_VERSION="$(grep VERSION_STR /usr/include/alsa/version.h 2>/dev/null | awk '{ print $3 }' | sed 's/"//g')" + + if [ -z "$ALSA_LIB_VERSION" ]; then + if [ -f /etc/lsb-release ]; then + . /etc/lsb-release + case "$DISTRIB_ID" in + Ubuntu) + if command -v dpkg > /dev/null ; then + ALSA_LIB_VERSION="$(dpkg -l libasound2 | tail -1 | awk '{ print $3 }' | cut -f 1 -d -)" + fi + + if [ "$ALSA_LIB_VERSION" = '' ]; then + ALSA_LIB_VERSION="" + fi + return + ;; + *) + return + ;; + esac + elif [ -f /etc/debian_version ]; then + if command -v dpkg > /dev/null ; then + ALSA_LIB_VERSION="$(dpkg -l libasound2 | tail -1 | awk '{ print $3 }' | cut -f 1 -d -)" + fi + + if [ "$ALSA_LIB_VERSION" = '' ]; then + ALSA_LIB_VERSION="" + fi + return + fi + fi +} + +# Basic requires +for prg in "${REQUIRES[@]}"; do + t="$(command -v "$prg")" + if test -z "$t"; then + echo "This script requires $prg utility to continue." + exit 1 + fi +done + +# Run checks to make sure the programs we need are installed. +LSPCI="$(command -v lspci)" +TPUT="$(command -v tput)" +DIALOG="$(command -v dialog)" + +# Check to see if sysfs is enabled in the kernel. We'll need this later on +SYSFS="$(mount | grep sysfs | awk '{ print $3 }')" + +# Check modprobe config files for sound related options +SNDOPTIONS="$(modprobe -c | sed -n 's/^options \(snd[-_][^ ]*\)/\1:/p')" + +KEEP_OUTPUT= +NFILE="" + +PASTEBIN="" +WWWSERVICE='www.alsa-project.org' +WELCOME=yes +PROCEED=yes +UPLOAD=ask +REPEAT="" +while [ -z "$REPEAT" ]; do +REPEAT=no +case "$1" in + --update|--help|--about) + WELCOME=no + PROCEED=no + ;; + --upload) + UPLOAD=yes + WELCOME=no + ;; + --no-upload) + UPLOAD=no + WELCOME=no + ;; + --pastebin) + PASTEBIN=yes + WWWSERVICE=pastebin + ;; + --no-dialog) + DIALOG="" + REPEAT="" + shift + ;; + --stdout) + DIALOG="" + WELCOME=no + ;; +esac +done + + +#Script header output. +if [ "$WELCOME" = yes ]; then +greeting_message="\ + +This script visits the following commands/files to collect diagnostic +information about your ALSA installation and sound related hardware. + + dmesg + lspci + aplay + amixer + alsactl + rpm, dpkg + /proc/asound/ + /sys/class/sound/ + ~/.asoundrc (etc.) + +See '$0 --help' for command line options. +" +if [ -n "$DIALOG" ]; then + dialog --backtitle "$BGTITLE" \ + --title "ALSA-Info script v $SCRIPT_VERSION" \ + --msgbox "$greeting_message" 20 80 +else + echo "ALSA Information Script v $SCRIPT_VERSION" + echo "--------------------------------" + echo "$greeting_message" +fi # dialog +fi # WELCOME + +# Set the output file +TEMPDIR="$(mktemp -t -d alsa-info.XXXXXXXXXX)" || exit 1 +FILE="$TEMPDIR/alsa-info.txt" +if [ -z "$NFILE" ]; then + NFILE="$(mktemp -t alsa-info.txt.XXXXXXXXXX)" || exit 1 +fi + +trap cleanup 0 + +if [ "$PROCEED" = yes ]; then + +if [ -z "$LSPCI" ]; then + if [ -d /sys/bus/pci ]; then + echo "This script requires lspci. Please install it, and re-run this script." + fi +fi + +# Fetch the info and store in temp files/variables +TSTAMP=$(LANG=C TZ=UTC date) +DISTRO=$(grep -ihs "buntu\|SUSE\|Fedora\|PCLinuxOS\|MEPIS\|Mandriva\|Debian\|Damn\|Sabayon\|Slackware\|KNOPPIX\|Gentoo\|Zenwalk\|Mint\|Kubuntu\|FreeBSD\|Puppy\|Freespire\|Vector\|Dreamlinux\|CentOS\|Arch\|Xandros\|Elive\|SLAX\|Red\|BSD\|KANOTIX\|Nexenta\|Foresight\|GeeXboX\|Frugalware\|64\|SystemRescue\|Novell\|Solaris\|BackTrack\|KateOS\|Pardus\|ALT" /etc/{issue,*release,*version}) +read -r KERNEL_RELEASE KERNEL_MACHINE KERNEL_PROCESSOR KERNEL_OS < <(uname -rpmo) +read -r KERNEL_VERSION < <(uname -v) +if [[ "$KERNEL_VERSION" = *SMP* ]]; then KERNEL_SMP=Yes; else KERNEL_SMP=No; fi +ALSA_DRIVER_VERSION=$(cat /proc/asound/version | head -n1 | awk '{ print $7 }' | sed 's/\.$//') +get_alsa_library_version +ALSA_UTILS_VERSION=$(amixer -v | awk '{ print $3 }') + +ESDINST=$(command -v esd) +PWINST=$(command -v pipewire) +PAINST=$(command -v pulseaudio) +ARTSINST=$(command -v artsd) +JACKINST=$(command -v jackd) +JACK2INST=$(command -v jackdbus) +ROARINST=$(command -v roard) +DMIDECODE=$(command -v dmidecode) + +#Check for DMI data +if [ -d /sys/class/dmi/id ]; then + # No root privileges are required when using sysfs method + DMI_SYSTEM_MANUFACTURER=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null) + DMI_SYSTEM_PRODUCT_NAME=$(cat /sys/class/dmi/id/product_name 2>/dev/null) + DMI_SYSTEM_PRODUCT_VERSION=$(cat /sys/class/dmi/id/product_version 2>/dev/null) + DMI_SYSTEM_FIRMWARE_VERSION=$(cat /sys/class/dmi/id/bios_version 2>/dev/null) + DMI_SYSTEM_SKU=$(cat /sys/class/dmi/id/product_sku 2>/dev/null) + DMI_BOARD_VENDOR=$(cat /sys/class/dmi/id/board_vendor 2>/dev/null) + DMI_BOARD_NAME=$(cat /sys/class/dmi/id/board_name 2>/dev/null) +elif [ -x $DMIDECODE ]; then + DMI_SYSTEM_MANUFACTURER=$($DMIDECODE -s system-manufacturer 2>/dev/null) + DMI_SYSTEM_PRODUCT_NAME=$($DMIDECODE -s system-product-name 2>/dev/null) + DMI_SYSTEM_PRODUCT_VERSION=$($DMIDECODE -s system-version 2>/dev/null) + DMI_SYSTEM_FIRMWARE_VERSION=$($DMIDECODE -s bios-version 2>/dev/null) + DMI_SYSTEM_SKU=$($DMIDECODE -s system-sku-number 2>/dev/null) + DMI_BOARD_VENDOR=$($DMIDECODE -s baseboard-manufacturer 2>/dev/null) + DMI_BOARD_NAME=$($DMIDECODE -s baseboard-product-name 2>/dev/null) +fi + +# Check for ACPI device status +if [ -d /sys/bus/acpi/devices ]; then + for f in /sys/bus/acpi/devices/*/status; do + ACPI_STATUS=$(cat $f 2>/dev/null); + if [[ "$ACPI_STATUS" -ne 0 ]]; then + echo $f $'\t' $ACPI_STATUS >>$TEMPDIR/acpidevicestatus.tmp; + fi + done +fi + +# Check for SoundWire ACPI _adr device status +if [ -d /sys/bus/acpi/devices ]; then + for f in /sys/bus/acpi/devices/*/adr; do + ACPI_ADR=$(cat $f 2>/dev/null); + if [[ "$ACPI_ADR" -ne 0 ]]; then + case $ACPI_ADR in + 0x??????025d*) echo "Realtek $ACPI_ADR" >>$TEMPDIR/sdwstatus.tmp;; + 0x??????01fa*) echo "Cirrus Logic $ACPI_ADR" >>$TEMPDIR/sdwstatus.tmp;; + 0x??????0102*) echo "TI $ACPI_ADR" >>$TEMPDIR/sdwstatus.tmp;; + esac + fi + done +fi + +awk '{ print $2 " (card " $1 ")" }' < /proc/asound/modules > $TEMPDIR/alsamodules.tmp 2> /dev/null +cat /proc/asound/cards > $TEMPDIR/alsacards.tmp +if [[ ! -z "$LSPCI" ]]; then + for class in 0401 0402 0403; do + lspci -vvnn -d "::$class" | sed -n '/^[^\t]/,+1p' + done > $TEMPDIR/lspci.tmp +fi + +#Check for HDA-Intel cards codec#* +cat /proc/asound/card*/codec\#* > $TEMPDIR/alsa-hda-intel.tmp 2> /dev/null + +#Check for AC97 cards codec +cat /proc/asound/card*/codec97\#0/ac97\#0-0 > $TEMPDIR/alsa-ac97.tmp 2> /dev/null +cat /proc/asound/card*/codec97\#0/ac97\#0-0+regs > $TEMPDIR/alsa-ac97-regs.tmp 2> /dev/null + +#Check for USB descriptors +if [ -x /usr/bin/lsusb ]; then + for f in /proc/asound/card[0-9]*/usbbus; do + test -f "$f" || continue + id=$(sed 's@/@:@' $f) + lsusb -v -s $id >> $TEMPDIR/lsusb.tmp 2> /dev/null + done +fi + +#Check for USB stream setup +cat /proc/asound/card*/stream[0-9]* > $TEMPDIR/alsa-usbstream.tmp 2> /dev/null + +#Check for USB mixer setup +cat /proc/asound/card*/usbmixer > $TEMPDIR/alsa-usbmixer.tmp 2> /dev/null + +#Fetch the info, and put it in $FILE in a nice readable format. +if [[ -z $PASTEBIN ]]; then +echo "upload=true&script=true&cardinfo=" > $FILE +else +echo "name=$USER&type=33&description=/tmp/alsa-info.txt&expiry=&s=Submit+Post&content=" > $FILE +fi +echo "!!################################" >> $FILE +echo "!!ALSA Information Script v $SCRIPT_VERSION" >> $FILE +echo "!!################################" >> $FILE +echo "" >> $FILE +echo "!!Script ran on: $TSTAMP" >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!Linux Distribution" >> $FILE +echo "!!------------------" >> $FILE +echo "" >> $FILE +echo $DISTRO >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!DMI Information" >> $FILE +echo "!!---------------" >> $FILE +echo "" >> $FILE +echo "Manufacturer: $DMI_SYSTEM_MANUFACTURER" >> $FILE +echo "Product Name: $DMI_SYSTEM_PRODUCT_NAME" >> $FILE +echo "Product Version: $DMI_SYSTEM_PRODUCT_VERSION" >> $FILE +echo "Firmware Version: $DMI_SYSTEM_FIRMWARE_VERSION" >> $FILE +echo "System SKU: $DMI_SYSTEM_SKU" >> $FILE +echo "Board Vendor: $DMI_BOARD_VENDOR" >> $FILE +echo "Board Name: $DMI_BOARD_NAME" >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!ACPI Device Status Information" >> $FILE +echo "!!---------------" >> $FILE +echo "" >> $FILE +cat $TEMPDIR/acpidevicestatus.tmp >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!ACPI SoundWire Device Status Information" >> $FILE +echo "!!---------------" >> $FILE +echo "" >> $FILE +cat $TEMPDIR/sdwstatus.tmp >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!Kernel Information" >> $FILE +echo "!!------------------" >> $FILE +echo "" >> $FILE +echo "Kernel release: $KERNEL_VERSION" >> $FILE +echo "Operating System: $KERNEL_OS" >> $FILE +echo "Architecture: $KERNEL_MACHINE" >> $FILE +echo "Processor: $KERNEL_PROCESSOR" >> $FILE +echo "SMP Enabled: $KERNEL_SMP" >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!ALSA Version" >> $FILE +echo "!!------------" >> $FILE +echo "" >> $FILE +echo "Driver version: $ALSA_DRIVER_VERSION" >> $FILE +echo "Library version: $ALSA_LIB_VERSION" >> $FILE +echo "Utilities version: $ALSA_UTILS_VERSION" >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!Loaded ALSA modules" >> $FILE +echo "!!-------------------" >> $FILE +echo "" >> $FILE +cat $TEMPDIR/alsamodules.tmp >> $FILE +echo "" >> $FILE +echo "" >> $FILE +echo "!!Sound Servers on this system" >> $FILE +echo "!!----------------------------" >> $FILE +echo "" >> $FILE +if [[ -n $PWINST ]];then +[[ $(pgrep '^(.*/)?pipewire$') ]] && PWRUNNING="Yes" || PWRUNNING="No" +echo "PipeWire:" >> $FILE +echo " Installed - Yes ($PWINST)" >> $FILE +echo " Running - $PWRUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -n $PAINST ]];then +[[ $(pgrep '^(.*/)?pulseaudio$') ]] && PARUNNING="Yes" || PARUNNING="No" +echo "Pulseaudio:" >> $FILE +echo " Installed - Yes ($PAINST)" >> $FILE +echo " Running - $PARUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -n $ESDINST ]];then +[[ $(pgrep '^(.*/)?esd$') ]] && ESDRUNNING="Yes" || ESDRUNNING="No" +echo "ESound Daemon:" >> $FILE +echo " Installed - Yes ($ESDINST)" >> $FILE +echo " Running - $ESDRUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -n $ARTSINST ]];then +[[ $(pgrep '^(.*/)?artsd$') ]] && ARTSRUNNING="Yes" || ARTSRUNNING="No" +echo "aRts:" >> $FILE +echo " Installed - Yes ($ARTSINST)" >> $FILE +echo " Running - $ARTSRUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -n $JACKINST ]];then +[[ $(pgrep '^(.*/)?jackd$') ]] && JACKRUNNING="Yes" || JACKRUNNING="No" +echo "Jack:" >> $FILE +echo " Installed - Yes ($JACKINST)" >> $FILE +echo " Running - $JACKRUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -n $JACK2INST ]];then +[[ $(pgrep '^(.*/)?jackdbus$') ]] && JACK2RUNNING="Yes" || JACK2RUNNING="No" +echo "Jack2:" >> $FILE +echo " Installed - Yes ($JACK2INST)" >> $FILE +echo " Running - $JACK2RUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -n $ROARINST ]];then +[[ $(pgrep '^(.*/)?roard$') ]] && ROARRUNNING="Yes" || ROARRUNNING="No" +echo "RoarAudio:" >> $FILE +echo " Installed - Yes ($ROARINST)" >> $FILE +echo " Running - $ROARRUNNING" >> $FILE +echo "" >> $FILE +fi +if [[ -z "$PAINST" && -z "$ESDINST" && -z "$ARTSINST" && -z "$JACKINST" && -z "$ROARINST" ]];then +echo "No sound servers found." >> $FILE +echo "" >> $FILE +fi +echo "" >> $FILE +echo "!!Soundcards recognised by ALSA" >> $FILE +echo "!!-----------------------------" >> $FILE +echo "" >> $FILE +cat $TEMPDIR/alsacards.tmp >> $FILE +echo "" >> $FILE +echo "" >> $FILE + +if [[ ! -z "$LSPCI" ]]; then +echo "!!PCI Soundcards installed in the system" >> $FILE +echo "!!--------------------------------------" >> $FILE +echo "" >> $FILE +cat $TEMPDIR/lspci.tmp >> $FILE +echo "" >> $FILE +echo "" >> $FILE +fi + +if [ "$SNDOPTIONS" ]; then +echo "!!Modprobe options (Sound related)" >> $FILE +echo "!!--------------------------------" >> $FILE +echo "" >> $FILE +modprobe -c|sed -n 's/^options \(snd[-_][^ ]*\)/\1:/p' >> $FILE +echo "" >> $FILE +echo "" >> $FILE +fi + +if [ -d "$SYSFS" ]; then + echo "!!Loaded sound module options" >> $FILE + echo "!!---------------------------" >> $FILE + echo "" >> $FILE + for mod in $(cat /proc/asound/modules | awk '{ print $2 }'); do + echo "!!Module: $mod" >> $FILE + for params in $(echo $SYSFS/module/$mod/parameters/*); do + echo -ne "\t" + value=$(cat $params) + echo "$params : $value" | sed 's:.*/::' + done >> $FILE + echo "" >> $FILE + done + echo "" >> $FILE + echo "!!Sysfs card info" >> $FILE + echo "!!---------------" >> $FILE + echo "" >> $FILE + for cdir in $(echo $SYSFS/class/sound/card*); do + echo "!!Card: $cdir" >> $FILE + driver=$(readlink -f "$cdir/device/driver") + echo "Driver: $driver" >> $FILE + echo "Tree:" >> $FILE + tree --noreport $cdir -L 2 | sed -e 's/^/\t/g' >> $FILE + echo "" >> $FILE + done + echo "" >> $FILE + if [ -d $SYSFS/class/sound/ctl-led ]; then + echo "!!Sysfs ctl-led info" >> $FILE + echo "!!---------------" >> $FILE + echo "" >> $FILE + for path in $(echo $SYSFS/class/sound/ctl-led/[ms][ip]*/card*); do + echo "!!CTL-LED: $path" >> $FILE + if [ -r "$path/list" ]; then + list=$(cat "$path/list") + echo "List: $list" >> $FILE + fi + #echo "Tree:" >> $FILE + #tree --noreport $path -L 2 | sed -e 's/^/\t/g' >> $FILE + echo "" >> $FILE + done + fi +fi + +if [ -s "$TEMPDIR/alsa-hda-intel.tmp" ]; then + echo "!!HDA-Intel Codec information" >> $FILE + echo "!!---------------------------" >> $FILE + echo "--startcollapse--" >> $FILE + echo "" >> $FILE + cat $TEMPDIR/alsa-hda-intel.tmp >> $FILE + echo "--endcollapse--" >> $FILE + echo "" >> $FILE + echo "" >> $FILE +fi + +if [ -s "$TEMPDIR/alsa-ac97.tmp" ]; then + echo "!!AC97 Codec information" >> $FILE + echo "!!----------------------" >> $FILE + echo "--startcollapse--" >> $FILE + echo "" >> $FILE + cat $TEMPDIR/alsa-ac97.tmp >> $FILE + echo "" >> $FILE + cat $TEMPDIR/alsa-ac97-regs.tmp >> $FILE + echo "--endcollapse--" >> $FILE + echo "" >> $FILE + echo "" >> $FILE +fi + +if [ -s "$TEMPDIR/lsusb.tmp" ]; then + echo "!!USB Descriptors" >> $FILE + echo "!!---------------" >> $FILE + echo "--startcollapse--" >> $FILE + cat $TEMPDIR/lsusb.tmp >> $FILE + echo "--endcollapse--" >> $FILE + echo "" >> $FILE + echo "" >> $FILE +fi + +if [ -s "$TEMPDIR/alsa-usbstream.tmp" ]; then + echo "!!USB Stream information" >> $FILE + echo "!!----------------------" >> $FILE + echo "--startcollapse--" >> $FILE + echo "" >> $FILE + cat $TEMPDIR/alsa-usbstream.tmp >> $FILE + echo "--endcollapse--" >> $FILE + echo "" >> $FILE + echo "" >> $FILE +fi + +if [ -s "$TEMPDIR/alsa-usbmixer.tmp" ]; then + echo "!!USB Mixer information" >> $FILE + echo "!!---------------------" >> $FILE + echo "--startcollapse--" >> $FILE + echo "" >> $FILE + cat $TEMPDIR/alsa-usbmixer.tmp >> $FILE + echo "--endcollapse--" >> $FILE + echo "" >> $FILE + echo "" >> $FILE +fi + +#If no command line options are specified, then run as though --with-all was specified +if [ -z "$1" ]; then + update +fi + +fi # proceed + +#loop through command line arguments, until none are left. +if [ -n "$1" ]; then + until [ -z "$1" ] + do + case "$1" in + --pastebin) + update + ;; + --update) + update + exit + ;; + --upload) + UPLOAD=yes + ;; + --no-upload) + UPLOAD=no + ;; + --output) + shift + NFILE="$1" + KEEP_OUTPUT=yes + ;; + --debug) + echo "Debugging enabled. $FILE and $TEMPDIR will not be deleted" + KEEP_FILES=yes + echo "" + ;; + --with-all) + withall + ;; + --with-aplay) + withaplay + WITHALL=no + ;; + --with-amixer) + withamixer + WITHALL=no + ;; + --with-alsactl) + withalsactl + WITHALL=no + ;; + --with-devices) + withdevices + WITHALL=no + ;; + --with-dmesg) + withdmesg + WITHALL=no + ;; + --with-configs) + withconfigs + WITHALL=no + ;; + --with-packages) + withpackages + WITHALL=no + ;; + --stdout) + UPLOAD=no + if [ -z "$WITHALL" ]; then + withall + fi + cat "$FILE" + rm "$FILE" + exit + ;; + --about) + echo "Written/Tested by the following users of #alsa on irc.freenode.net:" + echo "" + echo " wishie - Script author and developer / Testing" + echo " crimsun - Various script ideas / Testing" + echo " gnubien - Various script ideas / Testing" + echo " GrueMaster - HDA Intel specific items / Testing" + echo " olegfink - Script update function" + echo " TheMuso - display to stdout functionality" + exit 0 + ;; + *) + echo "alsa-info.sh version $SCRIPT_VERSION" + echo "" + echo "Available options:" + echo " --with-aplay (includes the output of aplay -l)" + echo " --with-amixer (includes the output of amixer)" + echo " --with-alsactl (includes the output of alsactl)" + echo " --with-configs (includes the output of ~/.asoundrc and" + echo " /etc/asound.conf if they exist)" + echo " --with-devices (shows the device nodes in /dev/snd/)" + echo " --with-dmesg (shows the ALSA/HDA kernel messages)" + echo " --with-packages (includes known packages installed)" + echo "" + echo " --output FILE (specify the file to output for no-upload mode)" + echo " --update (check server for script updates)" + echo " --upload (upload contents to remote server)" + echo " --no-upload (do not upload contents to remote server)" + echo " --pastebin (use 'https://pastebin.ca') as remote server" + echo " instead www.alsa-project.org" + echo " --stdout (print alsa information to standard output" + echo " instead of a file)" + echo " --about (show some information about the script)" + echo " --debug (will run the script as normal, but will not" + echo " delete ${FILE})" + exit 0 + ;; + esac + shift 1 + done +fi + +if [ "$PROCEED" = no ]; then + exit 1 +fi + +if [ -z "$WITHALL" ]; then + withall +fi + +# Check if wget is installed, and supports --post-file. +if ! wget --help 2>/dev/null | grep -q post-file; then + # We couldn't find a suitable wget. If --upload was passed, tell the user to upload manually. + if [ "$UPLOAD" != yes ]; then + : + elif [ -n "$DIALOG" ]; then + if [ -z "$PASTEBIN" ]; then + dialog --backtitle "$BGTITLE" --msgbox "Could not automatically upload output to 'https://www.alsa-project.org'.\nPossible reasons are:\n\n 1. Couldn't find 'wget' in your PATH\n 2. Your version of wget is less than 1.8.2\n\nPlease manually upload $NFILE to 'https://www.alsa-project.org/cardinfo-db' and submit your post." 25 100 + else + dialog --backtitle "$BGTITLE" --msgbox "Could not automatically upload output to 'https://www.pastebin.ca'.\nPossible reasons are:\n\n 1. Couldn't find 'wget' in your PATH\n 2. Your version of wget is less than 1.8.2\n\nPlease manually upload $NFILE to 'https://www.pastebin.ca/upload.php' and submit your post." 25 100 + fi + else + if [ -z "$PASTEBIN" ]; then + echo "" + echo "Could not automatically upload output to 'https://www.alsa-project.org'" + echo "Possible reasons are:" + echo " 1. Couldn't find 'wget' in your PATH" + echo " 2. Your version of wget is less than 1.8.2" + echo "" + echo "Please manually upload $NFILE to 'https://www.alsa-project.org/cardinfo-db' and submit your post." + echo "" + else + echo "" + echo "Could not automatically upload output to 'https://www.pastebin.ca'" + echo "Possible reasons are:" + echo " 1. Couldn't find 'wget' in your PATH" + echo " 2. Your version of wget is less than 1.8.2" + echo "" + echo "Please manually upload $NFILE to 'https://www.pastebin.ca/upload.php' and submit your post." + echo "" + fi + fi + UPLOAD=no +fi + +if [ "$UPLOAD" = ask ]; then + if [ -n "$DIALOG" ]; then + dialog --backtitle "$BGTITLE" --title "Information collected" --yes-label " UPLOAD / SHARE " --no-label " SAVE LOCALLY " --defaultno --yesno "\n\nAutomatically upload ALSA information to $WWWSERVICE?" 10 80 + DIALOG_EXIT_CODE="$?" + if [ "$DIALOG_EXIT_CODE" != 0 ]; then + UPLOAD=no + else + UPLOAD=yes + fi + else + echo -n "Automatically upload ALSA information to $WWWSERVICE? [y/N] : " + read -e CONFIRM + if [ "$CONFIRM" != y ]; then + UPLOAD=no + else + UPLOAD=yes + fi + fi + +fi + +if [ "$UPLOAD" = no ]; then + + mv -f "$FILE" "$NFILE" || exit 1 + KEEP_OUTPUT=yes + + if [[ -n "$DIALOG" ]] + then + dialog --backtitle "$BGTITLE" --title "Information collected" --msgbox "\n\nYour ALSA information is in $NFILE" 10 60 + else + echo "" + echo "Your ALSA information is in $NFILE" + echo "" + fi + + exit + +fi # UPLOAD + +if [[ -n "$DIALOG" ]] +then + dialog --backtitle "$BGTITLE" --infobox "Uploading information to $WWWSERVICE ..." 6 70 +else + echo -n "Uploading information to $WWWSERVICE ..." +fi + +if [[ -z "$PASTEBIN" ]]; then + wget -O - --tries=5 --timeout=60 --post-file="$FILE" 'https://www.alsa-project.org/cardinfo-db/' &> "$TEMPDIR/wget.tmp" +else + wget -O - --tries=5 --timeout=60 --post-file="$FILE" 'https://pastebin.ca/quiet-paste.php?api='"${PASTEBINKEY}"'&encrypt=t&encryptpw=blahblah' &> "$TEMPDIR/wget.tmp" +fi + +if [ "$?" -ne 0 ]; then + mv -f "$FILE" "$NFILE" || exit 1 + KEEP_OUTPUT=yes + + if [ -n "$DIALOG" ]; then + dialog --backtitle "$BGTITLE" --title "Information not uploaded" --msgbox "An error occurred while contacting $WWWSERVICE.\n Your information was NOT automatically uploaded.\n\nYour ALSA information is in $NFILE" 10 100 + else + echo "" + echo "An error occurred while contacting $WWWSERVICE." + echo "Your information was NOT automatically uploaded." + echo "" + echo "Your ALSA information is in $NFILE" + echo "" + fi + + exit +fi + +if [ -n "$DIALOG" ]; then + +dialog --backtitle "$BGTITLE" --title "Information uploaded" --yesno "Would you like to see the uploaded information?" 5 100 +DIALOG_EXIT_CODE="$?" +if [ "$DIALOG_EXIT_CODE" = 0 ]; then + grep -v alsa-info.txt "$FILE" > "$TEMPDIR/uploaded.txt" + dialog --backtitle "$BGTITLE" --textbox "$TEMPDIR/uploaded.txt" 0 0 +fi + +clear + +# no dialog +else + +echo -e " Done!" +echo "" + +fi # dialog + +if [ -z "$PASTEBIN" ]; then + FINAL_URL="$(grep 'SUCCESS:' "$TEMPDIR/wget.tmp" | cut -d ' ' -f 2)" +else + FINAL_URL="$(grep 'SUCCESS:' "$TEMPDIR/wget.tmp" | sed -n 's/.*\:\([0-9]\+\).*/https:\/\/pastebin.ca\/\1/p')" +fi + +# See if tput is available, and use it if it is. +if [ -x "$TPUT" ]; then + FINAL_URL="$(tput setaf 1; printf '%s' "$FINAL_URL"; tput sgr0)" +fi + +# Output the URL of the uploaded file. +echo "Your ALSA information is located at $FINAL_URL" +echo "Please inform the person helping you." +echo "" diff --git a/alsa-info/alsa-info.sh.8 b/alsa-info/alsa-info.sh.8 new file mode 100644 index 0000000..7168b3e --- /dev/null +++ b/alsa-info/alsa-info.sh.8 @@ -0,0 +1,62 @@ +.TH ALSA-INFO.SH 1 "13 January 2016" +.SH NAME +alsa-info.sh \- command\-line utility to gather information about +the ALSA subsystem +.SH SYNOPSIS +\fBalsa-info.sh\fP [\fIoptions\fP] + +.SH DESCRIPTION +\fBalsa-info.sh\fP is a command\-line utility gathering information +about the ALSA subsystem. It is used mostly for debugging purposes. + +.SH OPTIONS +.TP +\fI\-\-upload\fP +Upload contents to the server (www.alsa-project.org or pastebin.ca). +.TP +\fI\-\-no-upload\fP +Do not upload contents to the remote server. +.TP +\fI\-\-stdout\fP +Print information to standard output. +.TP +\fI\-\-output FILE\fP +Specify file for output in no-upload mode. +.TP +\fI\-\-debug\fP +Run utility as normal, but will not delete file (usually +/tmp/alsa-info.txt). +.TP +\fI\-\-with-aplay\fP +Includes output from \fIaplay -l\fP. +.TP +\fI\-\-with-amixer\fP +Includes output from \fIamixer\fP. +.TP +\fI\-\-with-alsactl\fP +Includes output from \fIalsactl\fP. +.TP +\fI\-\-with-configs\fP +Includes output from ~/.asoundrc and /etc/asound.conf if they exist. +.TP +\fI\-\-update\fP +Check server for updates. +.TP +\fI\-\-about\fP +Print information about authors. + +.SH EXAMPLES + +.TP +\fBalsa-info.sh \-\-no-upload\fR +Will gather all information and show the output file. + +.SH SEE ALSO +\fB +aplay(1) +amixer(1) +alsactl(1) +\fP + +.SH AUTHOR +\fBalsa-info.sh\fP was created by the ALSA team, see \fI\-\-about\fP . diff --git a/alsaconf/Makefile.am b/alsaconf/Makefile.am new file mode 100644 index 0000000..a4f5e4a --- /dev/null +++ b/alsaconf/Makefile.am @@ -0,0 +1,37 @@ +sbin_SCRIPTS = alsaconf +man_MANS = alsaconf.8 alsaconf.fr.8 +EXTRA_DIST = $(man_MANS) +SUBDIRS = po + +install-man8: + @for i in $(man_MANS); do \ + ext=`echo $$i | $(SED) -e 's/^.*\\.//'`; \ + inst=`echo $$i | $(SED) -e 's/\\.[^.]*$$//'`; \ + case $$inst in \ + *.[a-za-z]*)\ + loc=`echo $$inst | $(SED) -e 's/^.*\\.//'`; \ + loc="/$$loc"; \ + inst=`echo $$inst | $(SED) -e 's/\\.[^.]*$$//'`;; \ + *)\ + loc="";; \ + esac; \ + $(mkinstalldirs) $(DESTDIR)$(mandir)$$loc/man$$ext; \ + echo " $(INSTALL_DATA) $$i $(DESTDIR)$(mandir)$$loc/man$$ext/$$inst.$$ext"; \ + $(INSTALL_DATA) $(srcdir)/$$i $(DESTDIR)$(mandir)$$loc/man$$ext/$$inst.$$ext; \ + done + +uninstall-man8: + @for i in $(man_MANS); do \ + ext=`echo $$i | $(SED) -e 's/^.*\\.//'`; \ + inst=`echo $$i | $(SED) -e 's/\\.[^.]*$$//'`; \ + case $$inst in \ + *.[a-za-z]*)\ + loc=`echo $$inst | $(SED) -e 's/^.*\\.//'`; \ + loc="/$$loc"; \ + inst=`echo $$inst | $(SED) -e 's/\\.[^.]*$$//'`;; \ + *)\ + loc="";; \ + esac; \ + echo " rm -f $(DESTDIR)$(mandir)$$loc/man$$ext/$$inst.$$ext"; \ + rm -f $(DESTDIR)$(mandir)$$loc/man$$ext/$$inst.$$ext; \ + done diff --git a/alsaconf/alsaconf.8 b/alsaconf/alsaconf.8 new file mode 100644 index 0000000..a3e1296 --- /dev/null +++ b/alsaconf/alsaconf.8 @@ -0,0 +1,102 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" alsaconf.8 is copyright 2003 by Jordi Mallach +.\" +.\" This is free documentation, see the latest version of the GNU +.\" General Public License for copying conditions. There is NO warranty. +.TH ALSACONF 8 "February 23, 2003" + +.SH NAME +alsaconf \- configuration tool for the Advanced Linux Sound Architecture + +.SH SYNOPSIS +.B alsaconf +.RI [ options ] + +.SH DESCRIPTION +This manual page documents briefly the +.B alsaconf +command. +This manual page was written for the Debian distribution because the +original program does not have a manual page. +.PP +\fBAlsaconf\fP is a simple shell script which tries to detect the sound cards +on your system and writes a suitable configuration file for ALSA. It will try +to guess what GNU/Linux distribution you're running, and will act accordingly +to the standards of that distribution, if specific support is available. +.PP +Alsaconf will write a module-init-tools (or modutils) snippet which can be then used by module-init-tools (or modutils) +to load the correct parameters for your sound card. + +.SH OPTIONS +Alsaconf accepts the following options: +.TP +.B \-c, \-\-config file +Specify the module config file. +As default, +.B alsaconf +probes the available config file automatically. +.TP +.B \-d, \-\-devmode mode +Set the device mode for the ALSA devices (default = 0666). +This option is obsolete in the recent ALSA version. +.TP +.B \-g, \-\-gid gid +Set the gid for the ALSA devices (default = 0). +This option is obsolete in the recent ALSA version. +.TP +.B \-h, \-\-help +Displays this help text. +.TP +.B \-L, \-\-log file +Logging on the given file. The log is appended to the file. +This option is for debugging purpose only. +.TP +.B \-l, \-\-legacy +Check only for legacy non-isapnp cards. +.TP +.B \-m, \-\-modinfo +Read module descriptions instead of reading a card database. +.TP +.B \-P, \-\-listprobe +List the supported legacy card modules. +.TP +.B \-p, \-\-probe card-name +Probe a legacy non-isapnp card and print module options. +.TP +.B \-r, \-\-strict +Set strict device mode (equiv. with \-g 17 \-d 0660). +This option is obsolete in the recent ALSA version. +.TP +.B \-s, \-\-sound\-wav\-file +Use the specified wav file as a test sound. +.TP +.B \-u, \-\-uid uid +Set the uid for the ALSA devices (default = 0). +This option is obsolete in the recent ALSA version. + +.SH DEBIAN SPECIFIC +In Debian, the default gid of the device files is 29 (corresponding to the +audio group) and the default device mode is 0660. + +For the ALSA base package, see also +.I /usr/share/doc/alsa\-base/ + +.SH SEE ALSO +\fB +alsamixer(1), +amixer(1), +aplay(1), +arecord(1) +\fP + +.SH HOMEPAGE +http://www.alsa\-project.org/ + +.SH AUTHOR +The alsaconf script was written by +Takashi Iwai , +Bernd Kaindl and +Jan Ondrej (SAL) + +This manual page was written by Jordi Mallach , +for the Debian system (but may be used by others). diff --git a/alsaconf/alsaconf.fr.8 b/alsaconf/alsaconf.fr.8 new file mode 100644 index 0000000..14bbea8 --- /dev/null +++ b/alsaconf/alsaconf.fr.8 @@ -0,0 +1,111 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" alsaconf.8 is copyright 2003 by Jordi Mallach +.\" +.\" This is free documentation, see the latest version of the GNU +.\" General Public License for copying conditions. There is NO warranty. +.TH ALSACONF 8 "15 mars 2005" + +.SH NOM +alsaconf \- outil de configuration pour ALSA (architecture de son avancée de Linux) +.\" configuration tool for the Advanced Linux Sound Architecture + +.SH SYNOPSIS +.B alsaconf +.RI [ options ] + +.SH DESCRIPTION +Cette page de manuel documente brièvement la commande +.B alsaconf. + +.PP +\fBAlsaconf\fP +est un simple script shell qui essaye de détecter les cartes son sur votre +système et écrit un fichier de configuration adéquat pour ALSA. Il va +essayer de deviner quelle distribution GNU/Linux vous utilisez, et agira +conformément aux standards de cette distribution, si un support spécifique +est disponible. + +.PP +Alsaconf va écrire un fragment de code modutils qui peut être ensuite +utilisé par modutils pour charger les paramètres corrects de votre carte +son. + +.SH OPTIONS +Alsaconf accepte les options suivantes\ : + +.TP +.B \-l, \-\-legacy +Cherche seulement les anciennes cartes non-isapnp + +.TP +.B \-m, \-\-modinfo +Lit les descriptions de modules au lieu de lire une base de données de +carte + +.TP +.B \-s, \-\-sound\-wav\-file +Utilise le fichier wav spécifié comme son de test + +.TP +.B \-u, \-\-uid uid +Choisit l'uid pour les périphériques ALSA (valeur par défaut\ = 0) + +.TP +.B \-g, \-\-gid gid +Choisit le gid pour les périphériques ALSA (valeur par défaut\ = 0) + +.TP +.B \-d, \-\-devmode mode +Choisit le mode du périphérique pour les périphériques ALSA (valeur par +défaut\ : 0666) + +.TP +.B \-r, \-\-strict +Choisit le mode de périphérique strict (équivaut à -g\ 17 -d\ 0660) + +.TP +.B \-L, \-\-log +Mise en journal dans /tmp/alsaconf.log + +.TP +.B \-p, \-\-probe card-name +Teste une ancienne carte non-isapnp et affiche les options du module + +.TP +.B \-h, \-\-help +Affiche ce message d'aide + + +.SH POUR DEBIAN EXCLUSIVEMENT +Sous Debian, le gid par défaut des fichiers de périphériques est 29 (qui +correspond au groupe audio) et le mode par défaut des périphériques est +0660. + +Pour le paquetage de base ALSA, voir aussi +.I /usr/share/doc/alsa-base/ + +.SH VOIR AUSSI +\fB +alsamixer(1), +amixer(1), +aplay(1), +arecord(1) +\fP + +.SH PAGE D'ACCUEIL +http://www.alsa-project.org/ + +.SH AUTEUR +Le script alsaconf fut écrit par +Takashi Iwai , +Bernd Kaindl et +Jan Ondrej (SAL) + +Cette page de manuel fut écrite par Jordi Mallach , pour +le système Debian (mais elle peut être utilisée par d'autres). + +.SH TRADUCTION +Baptiste Mélès 2005. + +N'hésitez pas à signaler à l'auteur ou au traducteur, selon le cas, toute +erreur dans cette page de manuel. diff --git a/alsaconf/alsaconf.in b/alsaconf/alsaconf.in new file mode 100644 index 0000000..3d6adca --- /dev/null +++ b/alsaconf/alsaconf.in @@ -0,0 +1,1451 @@ +#!/bin/bash +# +# ALSA Configurator +# +# Copyright (c) 1999-2002 SuSE GmbH +# Jan ONDREJ +# +# written by Takashi Iwai +# Bernd Kaindl +# Jan ONDREJ (SAL) +# +# based on the original version of Jan ONDREJ's alsaconf for ALSA 0.4. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# + +export TEXTDOMAIN=alsaconf + +prefix=@prefix@ +exec_prefix=@exec_prefix@ +bindir=@bindir@ +sbindir=@sbindir@ +version=@VERSION@ +USE_NLS=@USE_NLS@ + +# Useful for debugging +PROCFS="/proc" +SYSFS="/sys" + +# i18n stuff +if test "$USE_NLS" = "yes" && type -p gettext > /dev/null; then + xecho() { + gettext -s "$*" + } +else + xecho() { + echo "$*" + } + gettext() { + echo -n "$*" + } +fi +xmsg() { + msg=$(gettext "$1") + shift + printf "$msg" $* +} + +# Check for GNU/Linux distributions +if [ -f /etc/SuSE-release ]; then + distribution="suse" + suse_version=$(grep 'VERSION = ' /etc/SuSE-release | sed -e s/'VERSION = '//) +elif [ -f /etc/UnitedLinux-release ]; then + distribution="suse" +elif [ -f /etc/gentoo-release ]; then + distribution="gentoo" +elif [ -f /etc/debian_version ]; then + distribution="debian" +elif [ -f /etc/mandrake-release ]; then + distribution="mandrake" +elif test -f /etc/redhat-release && grep -q "Red Hat" /etc/redhat-release; then + distribution="redhat" +elif test -f /etc/fedora-release && grep -q "Fedora" /etc/fedora-release; then + distribution="fedora" +elif [ -f /etc/slackware-version -o -f /etc/slamd64-version ]; then + distribution="slackware" +else + distribution="unknown" +fi + +for prog in lspci lsmod; do + for path in /sbin /usr/sbin /bin /usr/bin;do + test -x $path/$prog && eval $prog=$path/$prog + done +done +unset prog path + +usage() { + xecho "ALSA configurator" + echo " version $version" + xecho "usage: alsaconf [options] + -l|--legacy check only legacy non-isapnp cards + -m|--modinfo read module descriptions instead of reading card db + -s|--sound wav-file + use the specified wav file as a test sound + -u|--uid uid set the uid for the ALSA devices (default = 0) [obsoleted] + -g|--gid gid set the gid for the ALSA devices (default = 0) [obsoleted] + -d|--devmode mode + set the permission for ALSA devices (default = 0666) [obs.] + -r|--strict set strict device mode (equiv. with -g 17 -d 0660) [obsoleted] + -L|--log file logging on the specified file (for debugging purpose only) + -p|--probe card-name + probe a legacy non-isapnp card and print module options + -P|--listprobe list the supported legacy card modules + -c|--config file + specify the module config file + -R|--resources list available DMA and IRQ resources with debug for legacy + -h|--help what you're reading" +} + +OPTS=`getopt -o lmL:hp:Pu:g:d:rs:c:R --long legacy,modinfo,log:,help,probe:,listprobe,uid:,gid:,devmode:,strict,sound:,config:,resources -n alsaconf -- "$@"` || exit 1 +eval set -- "$OPTS" + +do_legacy_only=0 +use_modinfo_db=0 +alsa_uid=0 +alsa_gid=0 +alsa_mode=0666 +legacy_probe_card="" +LOGFILE="" +TESTSOUND="@TESTSOUND@" +try_all_combination=0 +resources="false" + +# legacy support +LEGACY_CARDS="opl3sa2 cs4236 cs4232 cs4231 es18xx es1688 sb16 sb8" + +while true ; do + case "$1" in + -l|--legacy) + do_legacy_only=1; shift ;; + -m|--modinfo) + use_modinfo_db=1; shift ;; + -s|--sound) + TESTSOUND=$2; shift 2;; + -h|--help) + usage; exit 0 ;; + -L|--log) + LOGFILE="$2"; shift 2;; + -p|--probe) + legacy_probe_card="$2"; shift 2;; + -P|--listprobe) + echo "$LEGACY_CARDS"; exit 0;; + -u|--uid) + alsa_uid="$2"; shift 2;; + -g|--gid) + alsa_gid="$2"; shift 2;; + -d|--devmode) + alsa_mode="$2"; shift 2;; + -r|--strict) + alsa_uid=0; alsa_gid=17; alsa_mode=0660; shift;; + -c|--config) + cfgfile="$2"; shift 2;; + -R|--resources) + resources="true"; shift;; + --) shift ; break ;; + *) usage ; exit 1 ;; + esac +done + +# +# probe legacy ISA cards +# + +check_dma_avail () { + list="" + if [ -d $SYSFS/bus/pnp/devices ]; then + for dma in $*; do + ok="true" + for i in $SYSFS/bus/pnp/devices/??:* ; do + if grep -q "state = active" $i/resources ; then + if grep -q '^dma '$dma'$' $i/resources; then + ok="false" + fi + fi + done + if [ -r $PROCFS/dma ]; then + if grep -q '^ *'$dma': ' $PROCFS/dma ; then + ok="false" + fi + fi + if [ "$ok" = "true" ]; then + list="$list $dma" + fi + done + else + if [ -r $PROCFS/dma ]; then + for dma in $*; do + grep -q '^ *'$dma': ' $PROCFS/dma || list="$list $dma" + done + fi + fi + if [ ! -z "$list" ]; then + echo $list + fi +} + +check_irq_avail () { + list="" + if [ -d $SYSFS/bus/pnp/devices ]; then + for irq in $*; do + ok="true" + for i in $SYSFS/bus/pnp/devices/??:* ; do + if grep -q "state = active" $i/resources ; then + if grep -q '^irq '$irq'$' $i/resources; then + ok="false" + fi + fi + done + if [ -r $PROCFS/interrupts ]; then + if grep -q '^ *'$irq': ' $PROCFS/interrupts ; then + ok="false" + fi + fi + if [ "$ok" = "true" ]; then + list="$list $irq" + fi + done + else + if [ -r $PROCFS/interrupts ]; then + for irq in $*; do + grep -q '^ *'$irq': ' $PROCFS/interrupts || list="$list $irq" + done + fi + fi + if [ ! -z "$list" ]; then + echo $list + fi +} + +# +# +# + +if [ "$resources" = "true" ]; then + if [ -d $SYSFS/bus/pnp/devices ]; then + for i in $SYSFS/bus/pnp/devices/??:* ; do + if [ "$resources" = "true" ]; then + echo ">>>>> PnP file: $i/resources" + cat $i/resources + fi + done + fi + if [ -r $PROCFS/dma ]; then + echo ">>>>> Allocated dma channels:" + cat $PROCFS/dma + fi + if [ -r $PROCFS/interrupts ]; then + echo ">>>>> Allocated interrupt channels:" + cat $PROCFS/interrupts + fi + echo -n "Valid DMA channels: " + check_dma_avail 0 1 2 3 4 5 6 7 + echo -n "Valid IRQ channels: " + check_irq_avail 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + exit 0 +fi + +# Check for root privileges +if [ `id -u` -ne 0 ]; then + xecho "You must be root to use this script." + exit 1 +fi + +# +# check the snd_ prefix for ALSA module options +# snd_ prefix is obsoleted since 0.9.0rc4. +# +if /sbin/modinfo -p snd | grep -q snd_ ; then + mpfx="snd_" +else + mpfx="" +fi + +alsa_device_opts="" +if /sbin/modinfo -p snd | grep -q uid ; then + if [ x"$alsa_uid" != x0 ]; then + alsa_device_opts="$alsa_device_opts ${mpfx}device_uid=$alsa_uid" + fi + if [ x"$alsa_gid" != x0 ]; then + alsa_device_opts="$alsa_device_opts ${mpfx}device_gid=$alsa_gid" + fi +fi +if /sbin/modinfo -p snd | grep -q device_mode ; then + if [ x"$alsa_mode" != x0 ]; then + alsa_device_opts="$alsa_device_opts ${mpfx}device_mode=$alsa_mode" + fi +fi + +case `uname -r` in +2.6.*) + kernel="new" + ;; +*) + kernel="old" + ;; +esac + +# cfgfile = base config file to remove/update the sound setting +# cfgout = new config file to write the sound setting (if different from $cfgfile) +cfgout="" +cfgoldout="" +if [ -n "$cfgfile" ]; then + if [ ! -r "$cfgfile" ]; then + xecho "ERROR: The config file doesn't exist: " + echo $cfgfile + exit 1 + fi +else +if [ "$kernel" = "new" ]; then + cfgfile="/etc/modprobe.conf" + if [ -d /etc/modprobe.d ]; then + cfgout="/etc/modprobe.d/50-sound.conf" + cfgoldout="/etc/modprobe.d/sound" + if [ ! -f $cfgout ]; then + case "$(modprobe -V)" in + "module-init-tools version "3.[789]*|\ + "module-init-tools version "3.1[0-9]*) + ;; + "kmod version "*) + ;; + *) + cfgout="/etc/modprobe.d/sound" + cfgoldout="" + ;; + esac + fi + fi +elif [ "$distribution" = "debian" ]; then + cfgfile="/etc/modutils/sound" +elif [ -e /etc/modules.conf ]; then + cfgfile="/etc/modules.conf" +elif [ -e /etc/conf.modules ]; then + cfgfile="/etc/conf.modules" +else + cfgfile="/etc/modules.conf" + touch /etc/modules.conf +fi +fi + +# Check for dialog, whiptail, gdialog, awk, ... ? +if type -p dialog > /dev/null; then + DIALOG=dialog +else + if type -p whiptail > /dev/null; then + whiptail_wrapper() { + X1="$1" + X2="$2" + if [ $1 = --yesno ]; then + X3=`expr $3 + 2` + else + X3=$3 + fi + shift 3 + whiptail "$X1" "$X2" $X3 "$@" + } + DIALOG=whiptail_wrapper + else + xecho "Error, dialog or whiptail not found." + exit 1 + fi +fi +if type -p awk > /dev/null; then : +else + xecho "Error, awk not found. Can't continue." + exit 1 +fi + +# +# remove entries by yast2 sound configurator +# +remove_y2_block() { + awk ' + /^alias sound-slot-[0-9]/ { next } + /^alias char-major-116 / { next } + /^alias char-major-14 / { next } + /^alias snd-card-[0-9] / { next } + /^options snd / { next } + /^options snd-/ { next } + /^options off / { next } + /^alias sound-service-[0-9]/ { next } + /^# YaST2: sound / { next } + { print }' +} + +# +# remove entries by sndconfig sound configurator +# +# found strings to search for in WriteConfModules, +# from sndconfig 0.68-4 (rawhide version) + +remove_sndconfig_block() { + awk ' + /^alias sound-slot-0/ { modulename = $3 ; next } + /^alias sound-slot-[0-9]/ { next } + /^post-install sound-slot-[0-9] / { next } + /^pre-remove sound-slot-[0-9] / { next } + /^options sound / { next } + /^alias synth0 opl3/ { next } + /^options opl3 / { next } + /^alias midi / { mididev = $3 ; next } + /^options / { if ($2 == mididev) next } + /^pre-install / { if ($2 == mididev) next } + /^alias synth0 / { synth = $3 ; next } + /^post-install / { if ($2 == synth) next } + /^options sb / { next } + /^post-install .+ \/bin\/modprobe "aci"/ { if ($2 == modulename) next } + /^options adlib_card / { next } + /^options .+ isapnp=1/ { if ($2 == modulename) next } + /^options i810_audio / { next } + /^options / {if ($2 == modulename) next } + { print }' +} + +# +# remove the previous configuration by alsaconf +# +remove_ac_block() { + awk '/^'"$ACB"'$/,/^'"$ACE"'$/ { next } { print }' +} + +# +# set default mixer volumes +# +set_mixers() { + amixer -s -q </dev/null 2>&1 + $rcalsasound unload >/dev/null 2>&1 + /sbin/rmmod dmasound dmasound_awacs 2>/dev/null +fi + + +cleanup () { + killall -9 aplay arecord >/dev/null 2>&1 + /sbin/modprobe -r isapnp >/dev/null 2>&1 + /sbin/modprobe -r isa-pnp >/dev/null 2>&1 + rm -f "$TMP" "$addcfg" "$FOUND" "$DUMP" +} +trap cleanup 0 + +TMP=`mktemp -q /tmp/alsaconf.XXXXXX` +if [ $? -ne 0 ]; then + xecho "Can't create temp file, exiting..." + exit 1 +fi +addcfg=`mktemp -q /tmp/alsaconf.XXXXXX` +if [ $? -ne 0 ]; then + xecho "Can't create temp file, exiting..." + exit 1 +fi +FOUND=`mktemp -q /tmp/alsaconf.XXXXXX` +if [ $? -ne 0 ]; then + xecho "Can't create temp file, exiting..." + exit 1 +fi +DUMP=`mktemp -q /tmp/alsaconf.XXXXXX` +if [ $? -ne 0 ]; then + xecho "Can't create temp file, exiting..." + exit 1 +fi + +# convert ISA PnP id number to string 'ABC' +convert_isapnp_id () { + if [ -z "$1" ]; then + echo "XXXX" + return + fi + let a='('$1'>>2) & 0x3f' + let b='(('$1' & 0x03) << 3) | (('$1' >> 13) & 0x07)' + let c='('$1'>> 8) & 0x1f' + strs='@ABCDEFGHIJKLMNOPQRSTUVWXYZ' + echo ${strs:$a:1}${strs:$b:1}${strs:$c:1} +} + +# swap high & low bytes +swap_number () { + if [ -z "$1" ]; then + echo "0000" + return + fi + let v='(('$1'>>8)&0xff)|(('$1'&0xff)<<8)' + printf "%04x" $v +} + +# build card database +# build_card_db filename +build_card_db () { + MODDIR=/lib/modules/`uname -r` + last_driver="" + echo -n > $1 + + # list pci cards + while read driver vendor device dummy; do + if expr $driver : 'snd-.*' >/dev/null ; then + if [ "$last_driver" != "$driver" ]; then + echo $driver.o + last_driver=$driver + fi + id1=`printf '0x%04x' $vendor` + id2=`printf '0x%04x' $device` + echo "PCI: $id1=$id2" + fi + done < $MODDIR/modules.pcimap >> $1 + + # list isapnp cards + while read driver cardvendor carddevice data vendor func; do + if expr $driver : 'snd-.*' >/dev/null ; then + if [ "$last_driver" != "$driver" ]; then + echo $driver.o + last_driver=$driver + fi + id1=`convert_isapnp_id $cardvendor` + dev1=`swap_number $carddevice` + id2=`convert_isapnp_id $vendor` + dev2=`swap_number $func` + echo "ISAPNP: $id1$dev1=$id2$dev2" + fi + done < $MODDIR/modules.isapnpmap >> $1 +} + +# +# probe cards +# +probe_cards () { + found="0" + test -r $PROCFS/isapnp || /sbin/modprobe isapnp >/dev/null 2>&1 + test -r $PROCFS/isapnp || /sbin/modprobe isa-pnp >/dev/null 2>&1 + if [ -r $PROCFS/isapnp ]; then + cat $PROCFS/isapnp >"$DUMP" + found="1" + elif [ -d $SYSFS/bus/pnp/devices ]; then + # use 2.6 kernel's sysfs output + # fake the isapnp dump + index=0 + bindex=0 + for d1 in $SYSFS/devices/pnp* ; do + for d2 in $d1/*:* ; do + if [ -r $d2/card_id ]; then + id=`cat $d2/card_id` + name=`cat $d2/name` + echo "Card $index '$id:$name' " >> "$DUMP" + index=$[$index+1] + found="1" + else if [ -r $d2/id ]; then + # FIXME: multiple id might be present (separated with new-line) + id=`head -n 1 $d2/id` + echo "BIOS $bindex '$id' " >> "$DUMP" + bindex=$[$bindex+1] + found="1" + fi + fi + done + done + fi + if [ "$found" = "0" ]; then + echo -n >"$DUMP" + fi + CARDID_DB=/var/tmp/alsaconf.cards + if [ ! -r $CARDID_DB ]; then + use_modinfo_db=1 + fi + if [ $use_modinfo_db != 1 ]; then + if [ $CARDID_DB -ot /lib/modules/`uname -r`/modules.dep ]; then + use_modinfo_db=1 + fi + fi + if [ $use_modinfo_db = 1 ]; then + xecho "Building card database.." + build_card_db $CARDID_DB + fi + if [ ! -r $CARDID_DB ]; then + xecho "No card database is found.." + exit 1 + fi + ncards=`grep '^snd-.*\.o$' $CARDID_DB | wc -w` + + msg=$(gettext "Searching sound cards") + awk ' +BEGIN { + format="%-40s %s\n"; + ncards='"$ncards"'; + idx=0; +} +/^snd-.*\.o$/{ + sub(/.o$/, ""); + driver=$0; + perc=(idx * 100) / (ncards + 1); + print int(perc); + idx++; +} +/^[]*PCI: /{ + gsub(/0x/, ""); + gsub(/=/, ":"); + x = sprintf ("'$lspci' -n 2>/dev/null| grep '"' 04..: '"' | grep %s", $2); + if (system (x) == 0) + printf "%s %s\n", $2, driver >>"'"$FOUND"'" +} +/^[]*ISAPNP: /{ + id2 = substr($0, index($0, "=")+1); + gsub(/=.*/, ""); + x = sprintf ("grep '\''^Card [0-9] .%s:'\'' '"$DUMP"'", $2); + if (system (x) == 0) + printf "%s %s\n", $2, driver >>"'"$FOUND"'" + else if (index($2, "ffff") > 0) { + x = sprintf ("grep '\''^BIOS [0-9]* .%s.'\'' '"$DUMP"'", id2); + if (system (x) == 0) + printf "%s %s\n", id2, driver >>"'"$FOUND"'" + } +}' < $CARDID_DB |\ + $DIALOG --gauge "$msg" 6 40 0 + + # + # PowerMac + # + if grep -q MacRISC $PROCFS/cpuinfo; then + MODDIR=/lib/modules/`uname -r` + find $MODDIR -name 'snd-powermac*' -print | \ + while read i; do + i=${i##*/} + i=${i%%.o} + i=${i%%.ko} + echo "PowerMac $i" >> $FOUND + done + fi + + # + # Sparc + # + if grep -q Sparc $PROCFS/cpuinfo; then + test -r $PROCFS/openprom/name || /bin/mount -t openpromfs none $PROCFS/openprom >/dev/null 2>&1 + # Check for an "audio" device + audio= + compat= + if test -r $PROCFS/openprom; then + audio=`find $PROCFS/openprom -follow -type d -name "audio*" -print` + fi + if test -n "$audio"; then + compat=`cat $audio/compatible` + compat=${compat#\'} + compat=${compat%\'} + compat=${compat#SUNW,} + fi + # Go through all cards we have + MODDIR=/lib/modules/`uname -r` + find $MODDIR -name 'snd-sun-*' -print | \ + while read i; do + i=${i##*/} + i=${i%%.o} + i=${i%%.ko} + sdev=`echo ${i#snd-sun-} | tr "[a-z]" "[A-Z]"` + + if test "$sdev" = "$compat"; then + echo "$sdev $i" >> $FOUND + elif test -r $PROCFS/openprom; then + find $PROCFS/openprom -follow -type d -name "SUNW,${sdev}*" \ + -exec echo "$sdev $i" \; 2>/dev/null >> $FOUND + else + echo "$sdev $i" >> $FOUND + fi + done + fi +} + +# +# look for a descriptive device name from the given device id +# +find_device_name () { + if expr "$1" : '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]:[0-9a-f][0-9a-f][0-9a-f][0-9a-f]' >/dev/null; then + $lspci -d $1 2>/dev/null| sed -e 's/^.*:..\.. [^:]*: //g' + return + elif expr "$1" : '[A-Z@][A-Z@][A-Z@][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' >/dev/null; then + cardname=`grep '^Card [0-9]\+ .'$1':' $DUMP | head -n 1 | sed -e 's/^Card [0-9]\+ '\''.*:\(.*\)'\'' .*$/\1/'` + echo $cardname + else + echo $1 + fi +} + +# get hwcfg file type from the given driver name +get_hwcfg_type () { + while read dev driver; do + if [ "$driver" = "$1" ]; then + case "$dev" in + *:*) + # FIXME: need to look around /sys/bus/pci/* (or use vpid-* ?) + devid=`$lspci -d "$dev" | head -n 1 | sed -e 's/ .*$//'` + case "$devid" in + *:*:*.*) ;; + *) devid="0000:$devid" ;; + esac + echo bus-pci-$devid + ;; + *) + echo $driver + ;; + esac + break + fi + done +} + +# clean up all hwcfg-* files containing ALSA modules +# alsaconf sets up exclusively +cleanup_hwcfg () { + for i in /etc/sysconfig/hardware/hwcfg-*; do + grep -q "MODULE='snd-" $i && rm -f $i + done +} + +# +# set up /etc/sysconfig/hardware/hwcfg-* stuff +# +setup_hwcfg () { + card=$1 + cleanup_hwcfg + cfg=`echo "$devs_olist" | get_hwcfg_type $card` + echo "MODULE='$card'" > /etc/sysconfig/hardware/hwcfg-$cfg + echo "STARTMODE='auto'" >> /etc/sysconfig/hardware/hwcfg-$cfg +} + + +# +# configure and try test sound +# +ac_config_card () { + + CARD_DRIVER=snd-$1 + CARD_OPTS="${*:2}" + + if [ -n "$cfgout" ]; then + msg=$(xmsg " +Configuring %s +Do you want to modify %s (and %s if present)?" $CARD_DRIVER $cfgout $cfgfile) + $DIALOG --yesno "$msg" 10 50 || acex 0 + else + msg=$(xmsg " +Configuring %s +Do you want to modify %s?" $CARD_DRIVER $cfgfile) + $DIALOG --yesno "$msg" 8 50 || acex 0 + fi + clear + + # Copy conf.modules and make changes. + ACB="# --- BEGIN: Generated by ALSACONF, do not edit. ---" + ACE="# --- END: Generated by ALSACONF, do not edit. ---" + + # Detect 2.2.X kernel + KVER=`uname -r | tr ".-" " "` + KVER1=`echo $KVER | cut -d" " -f1` + KVER2=`echo $KVER | cut -d" " -f2` + if [ $KVER1 -ge 2 ] && [ $KVER2 -ge 2 ]; then + SOUND_CORE="soundcore" + else + SOUND_CORE="snd" + fi + + if [ -r $cfgfile ] ; then + if [ "$distribution" = "redhat" -o "$distribution" = "fedora" ] ; then + remove_ac_block < $cfgfile | remove_sndconfig_block | uniq > $TMP + else + remove_ac_block < $cfgfile | remove_y2_block | uniq > $TMP + fi + fi + + if [ -z "$have_alias" -a "$kernel" = "new" ]; then + if grep -q char-major-116 /lib/modules/`uname -r`/modules.alias; then + have_alias="yes" + fi + fi + if [ -z "$have_alias" ]; then +echo "alias char-major-116 snd +alias char-major-14 $SOUND_CORE +alias sound-service-0-0 snd-mixer-oss +alias sound-service-0-1 snd-seq-oss +alias sound-service-0-3 snd-pcm-oss +alias sound-service-0-8 snd-seq-oss +alias sound-service-0-12 snd-pcm-oss" >> $addcfg + fi + if [ -n "$alsa_device_opts" ]; then + echo "options snd $alsa_device_opts" >> $addcfg + fi +echo "alias snd-card-0 $CARD_DRIVER +alias sound-slot-0 $CARD_DRIVER" >> $addcfg + if [ -n "$CARD_OPTS" ]; then + echo "options $CARD_DRIVER $CARD_OPTS" >> $addcfg + fi + + if [ -n "$cfgout" ]; then + [ ! -r "$cfgfile" ] || cmp -s "$TMP" "$cfgfile" || cat "$TMP" > "$cfgfile" + cmp -s "$addcfg" "$cfgout" || cat "$addcfg" > "$cfgout" + test -n "$cfgoldout" && rm -f "$cfgoldout" + else + echo "$ACB +# --- ALSACONF version $version ---" >> $TMP + cat "$addcfg" >> "$TMP" + echo "$ACE +" >> $TMP + cmp -s "$TMP" "$cfgfile" || cat "$TMP" > "$cfgfile" + fi + + /sbin/depmod -a 2>/dev/null + + # remove yast2 entries (- only for suse distro) + if [ -f /var/lib/YaST/unique.inf ]; then + awk ' +BEGIN { in_sound=0; } +/^\[sound\]$/ { print; in_sound=1; next; } +/^\[.+\]$/ { print; in_sound=0; next; } +{ if (in_sound == 0) { print; } } +' < /var/lib/YaST/unique.inf > $TMP + cp -f $TMP /var/lib/YaST/unique.inf + fi + + # set up /etc/sysconfig/hardware/* + if [ "$distribution" = "suse" ]; then + case "$suse_version" in + 10.[012]*|10) + setup_hwcfg $CARD_DRIVER + ;; + esac + fi + + farewell + clear + case "$distribution" in + gentoo | debian) + xecho "Running update-modules..." + update-modules + ;; + esac + if [ -x $rcalsasound ] ; then + echo Loading driver... + $rcalsasound restart + fi + echo Setting default volumes... + if [ -x $bindir/set_default_volume ]; then + $bindir/set_default_volume -f + else + set_mixers + fi + if [ -f $TESTSOUND ]; then + msg=$(gettext " + The mixer is set up now for for playing. + Shall I try to play a sound sample now? + + NOTE: +If you have a big amplifier, lower your volumes or say no. + Otherwise check that your speaker volume is open, + and look if you can hear test sound. +") + if $DIALOG --yesno "$msg" 13 65 + then + clear + echo + aplay -N $TESTSOUND + fi + fi + mkdir -p -m 0755 @ASOUND_STATE_DIR@ + if [ ! -r @ASOUND_STATE_DIR@/asound.state ]; then + xecho "Saving the mixer setup used for this in @ASOUND_STATE_DIR@/asound.state." + $sbindir/alsactl store + fi + clear + xecho " +=============================================================================== + + Now ALSA is ready to use. + For adjustment of volumes, use your favorite mixer. + + Have a lot of fun! + +" +} + +# check playback +# return 0 - OK, 1 - NG, 2 - not working (irq/dma problem) +ac_try_load () { + test -n "$LOGFILE" && echo "$1 ${*:2}" >> "$LOGFILE" + /sbin/modprobe snd-$1 ${*:2} >/dev/null 2>&1 + if $lsmod | grep -q -E '^(snd-|snd_)'$1' '; then + : ; + else + /sbin/modprobe -r snd-$1 >/dev/null 2>&1 + return 1 + fi + + # mute mixers + amixer set Master 0% mute >/dev/null 2>&1 + amixer set PCM 0% mute >/dev/null 2>&1 + + # output 0.5 sec + head -c 4000 < /dev/zero | aplay -N -r8000 -fS16_LE -traw -c1 > /dev/null 2>&1 & + # remember pid + pp=$! + # sleep for 2 seconds (to be sure -- 1 sec would be enough) + sleep 2 + # kill the child process if still exists. + kill -9 $pp > /dev/null 2>&1 + st=$? + ac_cardname=`head -n 1 $PROCFS/asound/cards | sed -e 's/^[0-9].* - \(.*\)$/\1/'` + /sbin/modprobe -r snd-$1 >/dev/null 2>&1 + if [ $st = 0 ]; then + # irq problem? + test -n "$LOGFILE" && echo "no playback return" >> "$LOGFILE" + return 2 + else + # seems ok! + test -n "$LOGFILE" && echo "playback OK" >> "$LOGFILE" + return 0 + fi +} + +# check capture +# return 0 - OK, 1 - NG, 2 - not working (irq/dma problem) +# ac_try_capture card duplex opts +ac_try_capture () { + test -n "$LOGFILE" && echo "$1 ${*:2}" >> "$LOGFILE" + /sbin/modprobe snd-$1 ${*:3} >/dev/null 2>&1 + if $lsmod | grep -q -E '^(snd-|snd_)'$1' '; then + : ; + else + /sbin/modprobe -r snd-$1 >/dev/null 2>&1 + return 1 + fi + + # mute mixers + amixer set Master 0% mute >/dev/null 2>&1 + amixer set PCM 0% mute >/dev/null 2>&1 + + play_pid=0 + if [ $2 = yes ]; then + # try duplex - start dummy playing + aplay -N -r8000 -fS16_LE -traw -c1 < /dev/zero > /dev/null 2>&1 & + play_pid=$! + fi + # record 1sec + arecord -N -d1 > /dev/null 2>&1 & + # remember pid + pp=$! + # sleep for 2 seconds + sleep 2 + # kill the child process if still exists. + kill -9 $pp > /dev/null 2>&1 + st=$? + # kill playback process if any + test $play_pid != 0 && kill -9 $play_pid + /sbin/modprobe -r snd-$1 >/dev/null 2>&1 + if [ $st = 0 ]; then + test -n "$LOGFILE" && echo "capture no return" >> "$LOGFILE" + return 2 + else + test -n "$LOGFILE" && echo "capture OK" >> "$LOGFILE" + return 0 + fi +} + +get_dma_pair () { + case $1 in + 0) + echo 1 3 5;; + 1) + echo 0 3 5;; + 3) + echo 1 0 5;; + 5) + echo 3 1 0;; + esac +} + +# +# check playback on specified irqs +# +# ac_try_irq card opts irqs... +# return 0 - OK, 1 - NG, 2 - not working (dma problem?) +# +ac_try_irq () { + card=$1 + opts="$2 ${mpfx}irq=$3" + ac_try_load $card $opts >/dev/null 2>&1 + result=$? + case $result in + 0) + ac_opts="$opts" + return 0 + ;; + 2) + for irq in ${*:4}; do + opts="$2 ${mpfx}irq=$irq" + ac_try_load $card $opts >/dev/null 2>&1 + if [ $? = 0 ]; then + ac_opts="$opts" + return 0 + fi + done + return 2 + ;; + esac + return 1 +} + +# +# check playback/capture on dma1 & dma2 & specified irqs +# +# ac_try_dmas card opts irqs... +# return 0 - OK, 1 - NG +# +ac_try_dmas () { + dma_list=`check_dma_avail 1 0 3 5` + for irq in ${*:3}; do + for dma1 in $dma_list; do + for dma2 in `get_dma_pair $dma1`; do + opts="$2 ${mpfx}dma1=$dma1 ${mpfx}dma2=$dma2 ${mpfx}irq=$irq" + ac_try_load $1 $opts >/dev/null 2>&1 + result=$? + if [ $result = 1 ]; then + if [ $try_all_combination = 1 ]; then + continue + else + return 1 + fi + elif [ $result = 0 ]; then + test -n "$LOGFILE" && echo "Now checking capture..." >> "$LOGFILE" + ac_opts="$opts" + ac_try_capture $1 yes $opts >/dev/null 2>&1 && return 0 + for d in yes no; do + for dma2 in $dma_list; do + if [ $dma1 != $dma2 ]; then + opts="$2 ${mpfx}dma1=$dma1 ${mpfx}dma2=$dma2 ${mpfx}irq=$irq" + ac_opts="$opts" + ac_try_capture $1 $d $opts >/dev/null 2>&1 && return 0 + fi + done + done + return 0 + fi + done + done + done + return 1 +} + +# check if the option $2 exists in card $1: set value $3 +ac_check_option () { + if /sbin/modinfo -p snd-$1 | grep -q $2; then + echo "$2=$3" + fi +} + +ac_try_card_sb8 () { + card=sb8 + irq_list=`check_irq_avail 5 3 9 10 7` + for dma8 in `check_dma_avail 1 3`; do + opts="${mpfx}dma8=$dma8" + ac_try_irq $card "$opts" $irq_list && return 0 + done + return 1 +} + +ac_try_card_sb16 () { + card=sb16 + isapnp=`ac_check_option $card ${mpfx}isapnp 0` + opts="$isapnp" + irq_list=`check_irq_avail 5 9 10 7 3` + dma_list=`check_dma_avail 0 1 3` + dma16_list=`check_dma_avail 5 6 7` + # at first try auto-probing by driver itself + ac_try_load $card $opts >/dev/null 2>&1 + result=$? + case $result in + 0) + ac_opts="$opts" + ac_try_capture $card yes $opts >/dev/null 2>&1 && return 0 + for d in yes no; do + for dma8 in $dma_list; do + for irq in $irq_list; do + opts="${mpfx}dma8=$dma8 ${mpfx}irq=$irq $isapnp" + ac_try_capture $card $d $opts >/dev/null 2>&1 && return 0 + done + done + done + return 0 + ;; + 2) + for dma16 in $dma16_list; do + opts="${mpfx}dma16=$dma16 $isapnp" + if ac_try_irq $card "$opts" $irq_list ; then + ac_try_capture $card yes $ac_opts >/dev/null 2>&1 && return 0 + ac_opts_saved="$ac_opts" + for d in yes no; do + for dma8 in $dma_list; do + ac_opts="$ac_opts_saved ${mpfx}dma8=$dma8" + ac_try_capture $card $d $ac_opts >/dev/null 2>&1 && return 0 + done + done + # return anyway here.. + return 0 + fi + done + ;; + esac + return 1 +} + +ac_try_card_es1688 () { + card=es1688 + opts="" + irq_list=`check_irq_avail 5 9 10 7` + for dma8 in `check_dma_avail 1 3 0`; do + opts="${mpfx}dma8=$dma8 ${mpfx}mpu_irq=-1" + ac_try_irq $card "$opts" $irq_list && return 0 + done + return 1 +} + +ac_try_card_es18xx () { + card=es18xx + opts=`ac_check_option $card ${mpfx}isapnp 0` + ac_try_dmas $card "$opts" `check_irq_avail 5 9 10 7` && return 0 + return 1 +} + +ac_try_card_cs4236 () { + card=cs4236 + irq_list=`check_irq_avail 5 7 9 11 12 15` + isapnp=`ac_check_option $card ${mpfx}isapnp 0` + for cport in 0x538 0x210 0xf00; do + for port in 0x530 0x534; do + opts="${mpfx}port=$port ${mpfx}cport=$cport $isapnp" + ac_try_dmas $card "$opts" $irq_list && return 0 + done + done + return 1 +} + +ac_try_card_cs4232 () { + card=cs4232 + irq_list=`check_irq_avail 5 7 9 11 12 15` + isapnp=`ac_check_option $card ${mpfx}isapnp 0` + for cport in 0x538 0x210 0xf00; do + for port in 0x530 0x534; do + opts="${mpfx}port=$port ${mpfx}cport=$cport $isapnp" + ac_try_dmas $card "$opts" $irq_list && return 0 + done + done + return 1 +} + +ac_try_card_cs4231 () { + card=cs4231 + irq_list=`check_irq_avail 5 7 9 11 12 15` + for port in 0x530 0x534; do + opts="${mpfx}port=$port" + ac_try_dmas $card "$opts" $irq_list && return 0 + done + return 1 +} + +ac_try_card_opl3sa2 () { + card=opl3sa2 + irq_list=`check_irq_avail 5 9 3 1 11 12 15 0` + isapnp=`ac_check_option $card ${mpfx}isapnp 0` + for port in 0x370 0x538 0xf86 0x100; do + for wss_port in 0x530 0xe80 0xf40 0x604; do + opts="${mpfx}fm_port=-1 ${mpfx}midi_port=-1 ${mpfx}port=$port ${mpfx}wss_port=$wss_port $isapnp" + ac_try_dmas $card "$opts" $irq_list && return 0 + done + done + return 1 +} + +ac_config_legacy () { + title=$(gettext "WARNING") + msg=$(gettext " + Probing legacy ISA cards might make + your system unstable. + + Do you want to proceed? + +") + $DIALOG --title "$title" --yesno "$msg" 10 50 || acex 0 + + if [ x"$1" = x ]; then + probe_list="$LEGACY_CARDS" + else + probe_list=$* + fi + menu_args=() + + for card in $probe_list; do + cardname=`/sbin/modinfo -d snd-$card | sed -e 's/^\"\(.*\)\"$/\1/g'` + if [ x"$cardname" != x ]; then + menu_args=("${menu_args[@]}" "$card" "$cardname" "on") + fi + done + if [ x$menu_args = x ]; then + msg=$(gettext "No legacy drivers are available + for your machine") + $DIALOG --msgbox "$msg" 5 50 + return 1 + fi + title=$(gettext "Driver Selection") + msg=$(gettext " Probing legacy ISA cards + + Please select the drivers to probe:") + $DIALOG --title "$title" --checklist "$msg" \ + 17 64 8 "${menu_args[@]}" 2> $FOUND || acex 0 + + if [ $try_all_combination != 1 ]; then + msg=$(gettext " + Shall I try all possible DMA and IRQ combinations? + With this option, some unconventional configuration + might be found, but it will take much longer time.") + if $DIALOG --yesno "$msg" 10 60 + then + try_all_combination=1 + fi + fi + + xecho "Probing legacy cards.. This may take a few minutes.." + echo -n $(gettext "Probing: ") + cards=`cat $FOUND | tr -d \"` + for card in $cards; do + echo -n " $card" + ac_opts="" + if eval ac_try_card_$card ; then + xecho " : FOUND!!" + ac_config_card $card $ac_opts + return 0 + fi + done + echo + title=$(gettext "Result") + msg=$(gettext "No legacy cards found") + $DIALOG --title "$title" --msgbox "$msg" 5 50 + return 1 +} + +# +# main part continued.. +# + +if test -n "$LOGFILE" ; then + touch "$LOGFILE" + echo -n "Starting alsaconf: " >> "$LOGFILE" + date >> "$LOGFILE" +fi + +if [ x"$legacy_probe_card" != x ]; then + ac_opts="" + if eval ac_try_card_$legacy_probe_card >/dev/null 2>&1; then + echo "$ac_opts" + echo "$ac_cardname" + exit 0 + else + echo "FAILED" + exit 1 + fi +fi + +intro + +if [ $do_legacy_only = 1 ]; then + ac_config_legacy + exit 0 +fi + +probe_cards + +devs_found=() +devs_olist="" + +if [ -s "$FOUND" ]; then + while read dev card ; do + MODDIR=/lib/modules/`uname -r` + find $MODDIR -type f | grep -q -E $card'\.(o|ko)' || continue + cardname=`find_device_name $dev | cut -c 1-64` + if [ -z "$cardname" ]; then + cardname="$card" + fi + card=${card##snd-} + devs_found=("${devs_found[@]}" "$card" "$cardname") + devs_devs=("${devs_devs[@]}" "$card" "$dev") + done <"$FOUND" + devs_olist=`cat $FOUND` +fi +if [ x$devs_found != x ]; then + # + # check for TP600E + # + if [ ${devs_found[0]} = cs46xx ]; then + if $lspci -nv 2>/dev/null| grep -q "Subsystem: 1014:1010"; then + msg=$(gettext " + Looks like you having a Thinkpad 600E or 770 notebook. + On this notebook, CS4236 driver should be used + although CS46xx chip is detected. + + Shall I try to snd-cs4236 driver and probe + the legacy ISA configuration?") + if $DIALOG --yesno "$msg" 13 60 + then + try_all_combination=1 + ac_config_legacy cs4236 + exit 0 + fi + elif $lspci -nv 2>/dev/null| grep -q "Subsystem: 8086:8080"; then + msg=$(gettext " + Looks like you having a Dell Dimension machine. + On this machine, CS4232 driver should be used + although CS46xx chip is detected. + + Shall I try to snd-cs4232 driver and probe + the legacy ISA configuration?") + if $DIALOG --yesno "$msg" 13 60 + then + try_all_combination=1 + ac_config_legacy cs4232 + exit 0 + fi + fi + fi + + devs_found=("${devs_found[@]}" "legacy" "Probe legacy ISA (non-PnP) chips") + title=$(gettext "Soundcard Selection") + msg=$(gettext " + Following card(s) are found on your system. + Choose a soundcard to configure: +") + $DIALOG --title "$title" --menu "$msg" 17 76 8 "${devs_found[@]}" --output-fd 3 3> $FOUND || acex 0 + card=`head -n 1 $FOUND` + if [ "$card" = "legacy" ]; then + ac_config_legacy + else + ac_config_card "$card" + fi + exit 0 +else + msg=$(gettext " + No supported PnP or PCI card found. + + Would you like to probe legacy ISA sound cards/chips? + +") + if $DIALOG --yesno "$msg" 9 60 ; then + ac_config_legacy + exit 0 + fi +fi + +rm -f "$FOUND" "$DUMP" +exit 0 diff --git a/alsaconf/po/eu.po b/alsaconf/po/eu.po new file mode 100644 index 0000000..cbf5e14 --- /dev/null +++ b/alsaconf/po/eu.po @@ -0,0 +1,381 @@ +# Basque translation for alsa-utils +# Copyright (C) 2011 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Porrumentzio , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: alsaconf\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2005-12-02 12:37+0100\n" +"PO-Revision-Date: 2020-09-11 09:40+0200\n" +"Last-Translator: Porrumentzio \n" +"Language-Team: Librezale \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: eu\n" +"X-Generator: Poedit 2.4.1\n" + +#: ../alsaconf.in:36 +msgid "-s" +msgstr "-s" + +#: ../alsaconf.in:80 +msgid "ALSA configurator" +msgstr "ALSA konfiguratzailea" + +#: ../alsaconf.in:82 +msgid "" +"usage: alsaconf [options]\n" +" -l|--legacy check only legacy non-isapnp cards\n" +" -m|--modinfo read module descriptions instead of reading card db\n" +" -s|--sound wav-file\n" +" use the specified wav file as a test sound\n" +" -u|--uid uid set the uid for the ALSA devices (default = 0) " +"[obsoleted]\n" +" -g|--gid gid set the gid for the ALSA devices (default = 0) " +"[obsoleted]\n" +" -d|--devmode mode\n" +" set the permission for ALSA devices (default = 0666) " +"[obs.]\n" +" -r|--strict set strict device mode (equiv. with -g 17 -d 0660) " +"[obsoleted]\n" +" -L|--log file logging on the specified file (for debugging purpose " +"only)\n" +" -p|--probe card-name\n" +" probe a legacy non-isapnp card and print module options\n" +" -P|--listprobe list the supported legacy card modules\n" +" -c|--config file\n" +" specify the module config file\n" +" -R|--resources list available DMA and IRQ resources with debug for " +"legacy\n" +" -h|--help what you're reading" +msgstr "" +"erabilera: alsaconf [aukerak]\n" +" -l|--legacy egiaztatu soilik isapnp ez diren txartel zaharrak\n" +" -m|--modinfo irakurri moduluen deskribapenak txartelen dBak beharrean\n" +" -s|--sound wav-fitxategia\n" +" erabili zehaztutako wav fitxategia soinu-proba gisa\n" +" -u|--uid UID ezarri ALSA gailuetarako UIDak (lehenetsia = 0) " +"[zaharkitua]\n" +" -g|--gid GID ezarri ALSA gailuetarako GIDak (lehenetsia = 0) " +"[zaharkitua]\n" +" -d|--devmode modua\n" +" ezarri ALSA gailuentzat baimenak (lehenetsia = 0666) " +"[zaharkitua]\n" +" -r|--strict ezarri gailu zorrotz modua (-g 17 -d 0660 komanduaren " +"baliokide [zahark.]\n" +" -L|--log fitxategia zehaztutako fitxategian gordetzea erregistroa " +"(arazte-xedetarako soilik)\n" +" -p|--probe txartel-izena\n" +" probatu isapnp ez diren txartel zaharrak eta erakutsi " +"moduluen aukerak\n" +" -P|--listprobe zerrendatu onartutako txartel zaharren moduluak\n" +" -c|--config fitxategia\n" +" zehaztu moduluen konfigurazio fitxategia\n" +" -R|--resources zerrendatu DMA eta IRQ baliabide eskuragarriak txartel " +"zaharrak arazteko\n" +" -h|--help laguntza-koadro hau" + +#: ../alsaconf.in:252 +msgid "You must be root to use this script." +msgstr "Root izan behar duzu script hau erabiltzeko." + +#: ../alsaconf.in:294 +msgid "ERROR: The config file doesn't exist: " +msgstr "ERROREA: Konfigurazio-fitxategia ez dago: " + +#: ../alsaconf.in:336 +msgid "Error, dialog or whiptail not found." +msgstr "Errorea: ez da aurkituelkarrizketa-koadro edo whiptail-ik." + +#: ../alsaconf.in:342 +msgid "Error, awk not found. Can't continue." +msgstr "Errorea: ez da aurkitu awk-ik. Ezin da aurrera egin." + +#: ../alsaconf.in:439 +msgid "" +"\n" +" ALSA CONFIGURATOR\n" +" version %s\n" +"\n" +" This script is a configurator for\n" +" Advanced Linux Sound Architecture (ALSA) driver.\n" +"\n" +"\n" +" If ALSA is already running, you should close all sound\n" +" apps now and stop the sound driver.\n" +" alsaconf will try to do this, but it's not 100%% sure." +msgstr "" +"\n" +" ALSA KONFIGURATZAILEA\n" +" %s bertsioa\n" +"\n" +" Script hau honetarako konfiguratzailea da:\n" +" Advanced Linux Sound Architecture (ALSA) kontrolatzailea.\n" +"\n" +"\n" +" ALSA jada lanean ari bada, itxi soinu-aplikazio guztiak\n" +" eta geldiarazi soinu-kontrolatzailea.\n" +" alsaconf hau burutzen saiatuko da, baina ez da erabat fidagarria." + +#: ../alsaconf.in:455 +msgid "" +"\n" +"\n" +" OK, sound driver is configured.\n" +"\n" +" ALSA CONFIGURATOR\n" +"\n" +" will prepare the card for playing now.\n" +"\n" +" Now I'll run alsasound init script, then I'll use\n" +" amixer to raise the default volumes.\n" +" You can change the volume later via a mixer\n" +" program such as alsamixer or gamix.\n" +" \n" +" " +msgstr "" +"\n" +"\n" +" Ados, soinu-kontrolatzailea konfiguratu da.\n" +"\n" +" ALSA KONFIGURATZAILEAk\n" +"\n" +" txartela erabiltzeko prest jarriko du.\n" +"\n" +" Orain alsasound init script-a abiaraziko du, ondoren\n" +" amixer erabiliko dut bolumen lehenetsiak igotzeko.\n" +" Bolumena gero aldatu dezakezu alsamixer edo gamix\n" +" bezalako nahasgailu-programa bat erabilita.\n" +" \n" +" " + +#: ../alsaconf.in:518 ../alsaconf.in:523 ../alsaconf.in:528 ../alsaconf.in:533 +msgid "Can't create temp file, exiting..." +msgstr "Ezin da aldi baterako fitxategirik sortu, irteten..." + +#: ../alsaconf.in:643 +msgid "Building card database.." +msgstr "Txartel-datu-basea eraikitzen..." + +#: ../alsaconf.in:647 +msgid "No card database is found.." +msgstr "Ez da txartel-datu-baserik aurkitu..." + +#: ../alsaconf.in:652 +msgid "Searching sound cards" +msgstr "Soinu-txartelak bilatzen" + +#: ../alsaconf.in:806 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s (and %s if present)?" +msgstr "" +"\n" +"%s konfiguratzen\n" +"%s aldatu nahi duzu (eta baita %s, baldin badago)?" + +#: ../alsaconf.in:811 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s?" +msgstr "" +"\n" +"%s konfiguratzen\n" +"%s aldatu nahi duzu?" + +#: ../alsaconf.in:900 +msgid "Running modules-update..." +msgstr "Modulu-eguneraketa egiten..." + +#: ../alsaconf.in:903 +msgid "Running update-modules..." +msgstr "Eguneraketa-moduluak egiten..." + +#: ../alsaconf.in:915 +msgid "" +"\n" +" The mixer is set up now for for playing.\n" +" Shall I try to play a sound sample now?\n" +"\n" +" NOTE:\n" +"If you have a big amplifier, lower your volumes or say no.\n" +" Otherwise check that your speaker volume is open,\n" +" and look if you can hear test sound.\n" +msgstr "" +"\n" +" Nahasgailua orain erreproduzitzeko konfiguratuta dago.\n" +" Nahi duzu orain bertan soinu-proba egitea?\n" +"\n" +" OHARRA:\n" +"Anplifikadore handirik baduzu, jaitsi bolumena edo erantzun ezetz.\n" +" Bestela ziurtatu zure bozgorailuaren bolumena aktibo dagoela\n" +" eta begiratu probako soinua entzun dezakezun.\n" + +#: ../alsaconf.in:932 +msgid "Saving the mixer setup used for this in /etc/asound.state." +msgstr "" +"Honetarako erabilitako nahasgailuaren konfigurazioa /etc/asound.state " +"fitxategian gordetzen." + +#: ../alsaconf.in:936 +msgid "" +"\n" +"===============================================================================\n" +"\n" +" Now ALSA is ready to use.\n" +" For adjustment of volumes, use your favorite mixer.\n" +"\n" +" Have a lot of fun!\n" +"\n" +msgstr "" +"\n" +"===============================================================================\n" +"\n" +" Orain ALSA erabiltzeko prest dago.\n" +" Bolumenak egokitzeko erabili nahi duzun nahasgailua.\n" +"\n" +" Gozatu!\n" +"\n" + +#: ../alsaconf.in:1244 +msgid "WARNING" +msgstr "ABISUA" + +#: ../alsaconf.in:1245 +msgid "" +"\n" +" Probing legacy ISA cards might make\n" +" your system unstable.\n" +"\n" +" Do you want to proceed?\n" +"\n" +msgstr "" +"\n" +" ISA txartel zaharrak probatzeak zure sistema\n" +" desegonkortu lezake.\n" +"\n" +" Aurrera egin nahi duzu?\n" +"\n" + +#: ../alsaconf.in:1268 +msgid "" +"No legacy drivers are available\n" +" for your machine" +msgstr "" +"Ez dago kontrolatzaile zaharrik\n" +" eskuragarri zure gailuan" + +#: ../alsaconf.in:1273 +msgid "Driver Selection" +msgstr "Kontrolatzaile hautapena" + +#: ../alsaconf.in:1274 +msgid "" +" Probing legacy ISA cards\n" +"\n" +" Please select the drivers to probe:" +msgstr "" +" ISA txartel zaharrak probatzea\n" +"\n" +" Hautatu probatzeko kontrolatzaileak:" + +#: ../alsaconf.in:1281 +msgid "" +"\n" +" Shall I try all possible DMA and IRQ combinations?\n" +" With this option, some unconventional configuration\n" +" might be found, but it will take much longer time." +msgstr "" +"\n" +"DMA eta IRQ konbinazio guztiak probatzea nahi duzu?\n" +"Aukera honekin konfigurazio ezohikoren bat aurkitu liteke,\n" +"baina denbora askoz ere gehiago hartuko du." + +#: ../alsaconf.in:1291 +msgid "Probing legacy cards.. This may take a few minutes.." +msgstr "Txartel zaharrak probatzen Honek minutu batzuk hartu litzake..." + +#: ../alsaconf.in:1292 +msgid "Probing: " +msgstr "Probatzen: " + +#: ../alsaconf.in:1298 +msgid " : FOUND!!" +msgstr " : AURKITU DA!!" + +#: ../alsaconf.in:1304 +msgid "Result" +msgstr "Emaitza" + +#: ../alsaconf.in:1305 +msgid "No legacy cards found" +msgstr "Ez da txartel zaharrik aurkitu" + +#: ../alsaconf.in:1364 +msgid "" +"\n" +" Looks like you having a Thinkpad 600E or 770 notebook.\n" +" On this notebook, CS4236 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4236 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +" Badirudi Thinkpad 600E edo 770 eramangarri bat duzula.\n" +" Eramangarri honetan CS4236 kontrolatzailea erabili beharko litzake\n" +" nahiz eta CS46xx txipa detektatu.\n" +"\n" +" Nahi duzu snd-cs4236 kontrolatzailea eta ISA\n" +" konfigurazio zaharra probatzea?" + +#: ../alsaconf.in:1378 +msgid "" +"\n" +" Looks like you having a Dell Dimension machine.\n" +" On this machine, CS4232 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4232 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +" Badirudi Dell Dimension gailua duzula.\n" +" Eramangarri honetan CS4232 kontrolatzailea erabili beharko litzake\n" +" nahiz eta CS46xx txipa detektatu.\n" +"\n" +" Nahi duzu snd-cs42362kontrolatzailea eta ISA\n" +" konfigurazio zaharra probatzea?" + +#: ../alsaconf.in:1395 +msgid "Soundcard Selection" +msgstr "Soinu-txatel hautapena" + +#: ../alsaconf.in:1396 +msgid "" +"\n" +" Following card(s) are found on your system.\n" +" Choose a soundcard to configure:\n" +msgstr "" +"\n" +" Txartel hau(ek) dago/daude zure sisteman.\n" +" Hautatu konfiguratzeko soinu-txartel bat:\n" + +#: ../alsaconf.in:1409 +msgid "" +"\n" +" No supported PnP or PCI card found.\n" +"\n" +" Would you like to probe legacy ISA sound cards/chips?\n" +"\n" +msgstr "" +"\n" +" Ez da aurkitu onartutako PnP edo PCI txartelik.\n" +"\n" +" Nahi duzu ISA soinu-txartel edo -txipak probatzea?\n" +"\n" diff --git a/alsaconf/po/ja.po b/alsaconf/po/ja.po new file mode 100644 index 0000000..170f708 --- /dev/null +++ b/alsaconf/po/ja.po @@ -0,0 +1,362 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: alsaconf\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2005-12-02 12:37+0100\n" +"PO-Revision-Date: 2005-12-02 12:39+0100\n" +"Last-Translator: Takashi Iwai \n" +"Language-Team: Japanese \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../alsaconf.in:36 +msgid "-s" +msgstr "" + +#: ../alsaconf.in:80 +msgid "ALSA configurator" +msgstr "ALSA 設定ツール" + +#: ../alsaconf.in:82 +msgid "" +"usage: alsaconf [options]\n" +" -l|--legacy check only legacy non-isapnp cards\n" +" -m|--modinfo read module descriptions instead of reading card db\n" +" -s|--sound wav-file\n" +" use the specified wav file as a test sound\n" +" -u|--uid uid set the uid for the ALSA devices (default = 0) [obsoleted]\n" +" -g|--gid gid set the gid for the ALSA devices (default = 0) [obsoleted]\n" +" -d|--devmode mode\n" +" set the permission for ALSA devices (default = 0666) " +"[obs.]\n" +" -r|--strict set strict device mode (equiv. with -g 17 -d 0660) " +"[obsoleted]\n" +" -L|--log file logging on the specified file (for debugging purpose only)\n" +" -p|--probe card-name\n" +" probe a legacy non-isapnp card and print module options\n" +" -P|--listprobe list the supported legacy card modules\n" +" -c|--config file\n" +" specify the module config file\n" +" -R|--resources list available DMA and IRQ resources with debug for legacy\n" +" -h|--help what you're reading" +msgstr "" +"使用法: alsaconf [オプション]\n" +" -l|--legacy éž ISAPnP カードã®ã¿ãƒã‚§ãƒƒã‚¯ã™ã‚‹\n" +" -m|--modinfo カード DB を読む代ã‚りã«ãƒ¢ã‚¸ãƒ¥ãƒ¼ãƒ«è¨˜è¿°ã‚’読ã¿è¾¼ã¿\n" +" -s|--sound WAVファイル\n" +" 指定㮠WAV ファイルをテスト音ã¨ã—ã¦ç”¨ã„ã‚‹\n" +" -u|--uid uid ALSA デãƒã‚¤ã‚¹ã® uid (デフォルト = 0) [旧オプション・無視ã•れã¾ã™]\n" +" -g|--gid gid ALSA デãƒã‚¤ã‚¹ã® gid (デフォルト = 0) [æ—§]\\n\"\n" +" -d|--devmode モード\n" +" ALSA デãƒã‚¤ã‚¹ã®ãƒ‘ーミッション(デフォルト = 0666) [æ—§]\n" +" -r|--strict 厳格ãªãƒ¢ãƒ¼ãƒ‰ (-g 17 -d 0660 ã¨åŒç­‰) [æ—§]\n" +" -L|--log file 指定ã®ãƒ•ァイルã«ãƒ­ã‚°ã‚’記録ã™ã‚‹ (デãƒãƒƒã‚°ç”¨é€”)\n" +" -p|--probe カードå\n" +" 指定㮠éžIsaPnP カードを検索ã—モジュールオプションを表示ã™ã‚‹\n" +" -P|--listprobe サãƒãƒ¼ãƒˆã•れã¦ã„ã‚‹æ—§å¼ã®(éžisapnp)カードã®ãƒ¢ã‚¸ãƒ¥ãƒ¼ãƒ«ä¸€è¦§è¡¨ç¤º\n" +" -c|--config ファイル\n" +" 指定ã®ãƒ¢ã‚¸ãƒ¥ãƒ¼ãƒ«è¨­å®šãƒ•ァイルを用ã„ã‚‹\n" +" -R|--resources レガシーデãƒã‚¤ã‚¹ã®ãƒ‡ãƒãƒƒã‚°ç”¨: å¯èƒ½ãª DMA 㨠IRQ を表示ã™ã‚‹\n" +" -h|--help 今見ã¦ã„ã¾ã™" + +#: ../alsaconf.in:252 +msgid "You must be root to use this script." +msgstr "ルートã®ã¿å®Ÿè¡Œå¯èƒ½ã§ã™" + +#: ../alsaconf.in:294 +msgid "ERROR: The config file doesn't exist: " +msgstr "エラー: 設定ファイルãŒå­˜åœ¨ã—ã¾ã›ã‚“: " + +#: ../alsaconf.in:336 +msgid "Error, dialog or whiptail not found." +msgstr "エラー: dialog ã¾ãŸã¯ whiptail ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" + +#: ../alsaconf.in:342 +msgid "Error, awk not found. Can't continue." +msgstr "エラー: awk ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" + +#: ../alsaconf.in:439 +msgid "" +"\n" +" ALSA CONFIGURATOR\n" +" version %s\n" +"\n" +" This script is a configurator for\n" +" Advanced Linux Sound Architecture (ALSA) driver.\n" +"\n" +"\n" +" If ALSA is already running, you should close all sound\n" +" apps now and stop the sound driver.\n" +" alsaconf will try to do this, but it's not 100%% sure." +msgstr "" +"\n" +" ALSA 設定ツール\n" +" ãƒãƒ¼ã‚¸ãƒ§ãƒ³ %s\n" +"\n" +" ã“ã®ã‚¹ã‚¯ãƒªãƒ—トã¯ã€Advanced Linux Sound Architecture\n" +" (ALSA) ドライãƒã®è¨­å®šã‚’行ã†ãƒ„ールã§ã™ã€‚\n" +"\n" +"\n" +" ã‚‚ã—æ—¢ã« ALSA ãŒèµ·å‹•ã—ã¦ã„ã‚‹å ´åˆã¯ã€å®Ÿè¡Œå‰ã«\n" +" å…¨ã¦ã®ã‚µã‚¦ãƒ³ãƒ‰ã‚¢ãƒ—リケーションを閉ã˜ã¦ã€ã‚µã‚¦ãƒ³ãƒ‰\n" +" ドライãƒã‚’åœæ­¢ã—ã¦ãã ã•ã„。\n" +" alsaconf ã®å‹•作㯠100%% ä¿è¨¼ã§ãã¾ã›ã‚“。" + +#: ../alsaconf.in:455 +msgid "" +"\n" +"\n" +" OK, sound driver is configured.\n" +"\n" +" ALSA CONFIGURATOR\n" +"\n" +" will prepare the card for playing now.\n" +"\n" +" Now I'll run alsasound init script, then I'll use\n" +" amixer to raise the default volumes.\n" +" You can change the volume later via a mixer\n" +" program such as alsamixer or gamix.\n" +" \n" +" " +msgstr "" +"\n" +"\n" +" サウンドドライãƒãŒè¨­å®šã•れã¾ã—ãŸã€‚\n" +"\n" +" ã“れã‹ã‚‰ã‚«ãƒ¼ãƒ‰ã®å†ç”Ÿã®æº–備を行ã„ã¾ã™ã€‚\n" +"\n" +"\n" +" alsasound スクリプトを実行ã—ã€ãã®å¾Œ amixer ã§\n" +" デフォルトã®éŸ³é‡ã«è¨­å®šã—ã¾ã™ã€‚\n" +" 後ã§ã€alsamixer ã‚„ gamix ãªã©ã®ãƒŸã‚­ã‚µãƒ¼ãƒ—ログラム\n" +" を用ã„ã¦ã€éŸ³é‡ã‚’変更ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚\n" +" " + +#: ../alsaconf.in:518 ../alsaconf.in:523 ../alsaconf.in:528 ../alsaconf.in:533 +msgid "Can't create temp file, exiting..." +msgstr "一時ファイルを作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚中止ã—ã¾ã™..." + +#: ../alsaconf.in:643 +msgid "Building card database.." +msgstr "カードデータベースを作æˆä¸­..." + +#: ../alsaconf.in:647 +msgid "No card database is found.." +msgstr "カードãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸ..." + +#: ../alsaconf.in:652 +msgid "Searching sound cards" +msgstr "サウンドカードを検索中" + +#: ../alsaconf.in:806 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s (and %s if present)?" +msgstr "" +"\n" +"%s を設定ã—ã¾ã™\n" +"%s (ãŠã‚ˆã³ %s) を変更ã—ã¾ã™ã‹ï¼Ÿ" + +#: ../alsaconf.in:811 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s?" +msgstr "" +"\n" +"%s を設定ã—ã¾ã™\n" +"%s を変更ã—ã¾ã™ã‹ï¼Ÿ" + +#: ../alsaconf.in:900 +msgid "Running modules-update..." +msgstr "modules-update を実行中..." + +#: ../alsaconf.in:903 +msgid "Running update-modules..." +msgstr "update-modules を実行中..." + +#: ../alsaconf.in:915 +msgid "" +"\n" +" The mixer is set up now for for playing.\n" +" Shall I try to play a sound sample now?\n" +"\n" +" NOTE:\n" +"If you have a big amplifier, lower your volumes or say no.\n" +" Otherwise check that your speaker volume is open,\n" +" and look if you can hear test sound.\n" +msgstr "" +"\n" +" 音é‡ã‚’å†ç”Ÿç”¨ã«æº–å‚™ã—終ãˆã¾ã—ãŸã€‚\n" +" サンプルã®ã‚µã‚¦ãƒ³ãƒ‰ã‚’å†ç”Ÿã—ã¾ã™ã‹ï¼Ÿ\n" +"\n" +" 注æ„:\n" +" 大容é‡ã‚¢ãƒ³ãƒ—ã®å ´åˆã¯ã€éŸ³é‡ã‚’下ã’ã¦ãŠãã‹ã€ã“ã“ã§ã€Œã„ã„ãˆã€\n" +" ã‚’é¸æŠžã—ã¦ãã ã•ã„。スピーカーã®éŸ³é‡ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¦ã€\n" +" テスト音ãŒèžã“ãˆã‚‹ã‹ã©ã†ã‹ç¢ºèªã—ã¦ãã ã•ã„。\n" + +#: ../alsaconf.in:932 +msgid "Saving the mixer setup used for this in /etc/asound.state." +msgstr "ミキサーã®è¨­å®šã‚’ /etc/asound.state ã«ä¿å­˜ã—ã¾ã™" + +#: ../alsaconf.in:936 +msgid "" +"\n" +"===============================================================================\n" +"\n" +" Now ALSA is ready to use.\n" +" For adjustment of volumes, use your favorite mixer.\n" +"\n" +" Have a lot of fun!\n" +"\n" +msgstr "" +"\n" +"===============================================================================\n" +"\n" +" ã“れ㧠ALSA ãŒä½¿ç”¨å¯èƒ½ã«ãªã‚Šã¾ã—ãŸã€‚\n" +" 音é‡ã®èª¿æ•´ã«ã¯ã€ãƒŸã‚­ã‚µãƒ¼ãƒ—ログラムをã”使用下ã•ã„。\n" +"\n" + +#: ../alsaconf.in:1244 +msgid "WARNING" +msgstr "注æ„" + +#: ../alsaconf.in:1245 +msgid "" +"\n" +" Probing legacy ISA cards might make\n" +" your system unstable.\n" +"\n" +" Do you want to proceed?\n" +"\n" +msgstr "" +"\n" +" æ—§å¼ã® ISA ã‚«ãƒ¼ãƒ‰ã®æ¤œç´¢ã‚’行ã†ã¨\n" +" システムãŒä¸å®‰å®šã«ãªã‚‹å ´åˆãŒã‚りã¾ã™ã€‚\n" +"\n" +" ã“ã®ã¾ã¾ç¶šã‘ã¾ã™ã‹ï¼Ÿ\n" +"\n" + +#: ../alsaconf.in:1268 +msgid "" +"No legacy drivers are available\n" +" for your machine" +msgstr "" +"æ—§å¼ã® ISA カードã¯\n" +"検出ã•れã¾ã›ã‚“ã§ã—ãŸ" + +#: ../alsaconf.in:1273 +msgid "Driver Selection" +msgstr "ドライãƒã®é¸æŠž" + +#: ../alsaconf.in:1274 +msgid "" +" Probing legacy ISA cards\n" +"\n" +" Please select the drivers to probe:" +msgstr "" +" æ—§å¼ã® ISA ã‚«ãƒ¼ãƒ‰ã®æ¤œå‡º\n" +"\n" +" 検出ã™ã‚‹ãƒ‰ãƒ©ã‚¤ãƒã‚’é¸æŠžã—ã¦ãã ã•ã„:" + +#: ../alsaconf.in:1281 +msgid "" +"\n" +" Shall I try all possible DMA and IRQ combinations?\n" +" With this option, some unconventional configuration\n" +" might be found, but it will take much longer time." +msgstr "" +"\n" +" å¯èƒ½ãª DMA 㨠IRQ ã®å…¨ã¦ã®çµ„åˆã›ã‚’試ã¿ã¾ã™ã‹ï¼Ÿ\n" +" ã“ã®ã‚ªãƒ—ションã§ã¯ã€ã¾ã‚Œãªè¨­å®šã‚‚検出ã•れるã“ã¨ãŒ\n" +" å¯èƒ½ã§ã™ãŒã€ã‚ˆã‚Šé•·ã„時間ãŒã‹ã‹ã‚Šã¾ã™ã€‚" + +#: ../alsaconf.in:1291 +msgid "Probing legacy cards.. This may take a few minutes.." +msgstr "æ—§å¼ã‚«ãƒ¼ãƒ‰ã®æ¤œå‡ºä¸­... 長時間ãŒã‹ã‹ã‚‹ã“ã¨ãŒã‚りã¾ã™..." + +#: ../alsaconf.in:1292 +msgid "Probing: " +msgstr "検出中: " + +#: ../alsaconf.in:1298 +msgid " : FOUND!!" +msgstr " : 発見!!" + +#: ../alsaconf.in:1304 +msgid "Result" +msgstr "çµæžœ" + +#: ../alsaconf.in:1305 +msgid "No legacy cards found" +msgstr "æ—§å¼ã‚«ãƒ¼ãƒ‰ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸ" + +#: ../alsaconf.in:1364 +msgid "" +"\n" +" Looks like you having a Thinkpad 600E or 770 notebook.\n" +" On this notebook, CS4236 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4236 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +" Thinkpad 600E ã¾ãŸã¯ 770 ã‚’ã”使用ã®ã‚ˆã†ã§ã™ã€‚\n" +" ã“ã®ãƒžã‚·ãƒ³ã§ã¯ã€CS46xx ãƒãƒƒãƒ—ãŒæ¤œå‡ºã•れã¾ã™ãŒ\n" +" CS4236 ドライãƒã‚’用ã„ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚\n" +"\n" +" snd-cs4236 ドライãƒã‚’試用ã—ã¦ã€æ—§å¼ã® ISA 設定\n" +" ã®æ¤œå‡ºã‚’行ã„ã¾ã™ã‹ï¼Ÿ" + +#: ../alsaconf.in:1378 +msgid "" +"\n" +" Looks like you having a Dell Dimension machine.\n" +" On this machine, CS4232 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4232 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +" Dell Dimension マシンをã”使用ã®ã‚ˆã†ã§ã™ã€‚\n" +" ã“ã®ãƒžã‚·ãƒ³ã§ã¯ã€CS46xx ãƒãƒƒãƒ—ãŒæ¤œå‡ºã•れã¾ã™ãŒ\n" +" CS4232 ドライãƒã‚’用ã„ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚\n" +"\n" +" snd-cs4232 ドライãƒã‚’試用ã—ã¦ã€æ—§å¼ã® ISA 設定\n" +" ã®æ¤œå‡ºã‚’行ã„ã¾ã™ã‹ï¼Ÿ" + +#: ../alsaconf.in:1395 +msgid "Soundcard Selection" +msgstr "サウンドカードã®é¸æŠž" + +#: ../alsaconf.in:1396 +msgid "" +"\n" +" Following card(s) are found on your system.\n" +" Choose a soundcard to configure:\n" +msgstr "" +"\n" +" 以下ã®ã‚«ãƒ¼ãƒ‰ãŒã‚·ã‚¹ãƒ†ãƒ ã‹ã‚‰æ¤œå‡ºã•れã¾ã—ãŸã€‚\n" +" 設定ã™ã‚‹ã‚«ãƒ¼ãƒ‰ã‚’é¸æŠžã—ã¦ãã ã•ã„:\n" + +#: ../alsaconf.in:1409 +msgid "" +"\n" +" No supported PnP or PCI card found.\n" +"\n" +" Would you like to probe legacy ISA sound cards/chips?\n" +"\n" +msgstr "" +"\n" +" PnP ã¾ãŸã¯ PCI ã‚«ãƒ¼ãƒ‰ãŒæ¤œå‡ºã•れã¾ã›ã‚“ã§ã—ãŸã€‚\n" +"\n" +" æ—§å¼ã® ISA ã‚µã‚¦ãƒ³ãƒ‰ã‚«ãƒ¼ãƒ‰ã®æ¤œå‡ºã‚’行ã„ã¾ã™ã‹ï¼Ÿ\n" diff --git a/alsaconf/po/ka.po b/alsaconf/po/ka.po new file mode 100644 index 0000000..a9815c7 --- /dev/null +++ b/alsaconf/po/ka.po @@ -0,0 +1,378 @@ +# Georgian translation for alsaconf. +# Copyright (C) 2022 alsaconf's authors +# This file is distributed under the same license as the alsaconf package. +# Temuri Doghonadze , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: alsaconf\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-10-22 22:12+0200\n" +"PO-Revision-Date: 2022-10-22 22:41+0200\n" +"Last-Translator: Temuri Doghonadze \n" +"Language-Team: Georgian <(nothing)>\n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2\n" + +#: ../alsaconf.in:36 +msgid "-s" +msgstr "-s" + +#: ../alsaconf.in:82 +msgid "ALSA configurator" +msgstr "ALSA -ის მáƒáƒ áƒ’ებáƒ" + +#: ../alsaconf.in:84 +msgid "" +"usage: alsaconf [options]\n" +" -l|--legacy check only legacy non-isapnp cards\n" +" -m|--modinfo read module descriptions instead of reading card db\n" +" -s|--sound wav-file\n" +" use the specified wav file as a test sound\n" +" -u|--uid uid set the uid for the ALSA devices (default = 0) " +"[obsoleted]\n" +" -g|--gid gid set the gid for the ALSA devices (default = 0) " +"[obsoleted]\n" +" -d|--devmode mode\n" +" set the permission for ALSA devices (default = 0666) " +"[obs.]\n" +" -r|--strict set strict device mode (equiv. with -g 17 -d 0660) " +"[obsoleted]\n" +" -L|--log file logging on the specified file (for debugging purpose " +"only)\n" +" -p|--probe card-name\n" +" probe a legacy non-isapnp card and print module options\n" +" -P|--listprobe list the supported legacy card modules\n" +" -c|--config file\n" +" specify the module config file\n" +" -R|--resources list available DMA and IRQ resources with debug for " +"legacy\n" +" -h|--help what you're reading" +msgstr "" +"გáƒáƒ›áƒáƒ§áƒ”ნებáƒ: alsaconf [პáƒáƒ áƒáƒ›áƒ”ტრები]\n" +" -l|--legacy მხáƒáƒšáƒáƒ“ მáƒáƒ«áƒ•ელებული áƒáƒ áƒ-isanpn ბáƒáƒ áƒáƒ—ების ძებნáƒ\n" +" -m|--modinfo ბáƒáƒ áƒáƒ—ების ბáƒáƒ–ის მáƒáƒ’იერ მáƒáƒ“ულების áƒáƒ¦áƒ¬áƒ”რების წáƒáƒ™áƒ˜áƒ—ხვáƒ\n" +" -s|--sound wav-file\n" +" სáƒáƒ¢áƒ”სტრხმáƒáƒ“ მითითებული WAV ფáƒáƒ˜áƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ\n" +" -u|--uid uid ALSA -ის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების UID-ის მითითებრ(ნáƒáƒ’ულისხმები = 0) " +"[მáƒáƒ«áƒ•ელებულიáƒ]\n" +" -g|--gid gid ALSA -ის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების GID-ის მითითებრ(ნáƒáƒ’ულისხმები = 0) " +"[მáƒáƒ«áƒ•ელებულიáƒ]\n" +" -d|--devmode რეჟიმი\n" +" ALSA-ის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების ფáƒáƒ˜áƒšáƒ”ბის წვდáƒáƒ›áƒ˜áƒ¡ მითითებრ" +"(ნáƒáƒ’ულისხმები = 0666) [მáƒáƒ«áƒ•ელებულიáƒ]\n" +" -r|--strict მკáƒáƒªáƒ áƒ˜ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის რეჟიმი (იგივე, რáƒáƒª -g 17 -d 0660) " +"[მáƒáƒ«áƒ•ელებულიáƒ]\n" +" -L|--log ფáƒáƒ˜áƒšáƒ˜ ჟურნáƒáƒšáƒ˜áƒ¡ მითითებულ ფáƒáƒ˜áƒšáƒ¨áƒ˜ ჩáƒáƒ¬áƒ”რრ(მხáƒáƒšáƒáƒ“ გáƒáƒ›áƒáƒ áƒ—ვისáƒáƒ¡)\n" +" -p|--probe card-name\n" +" მáƒáƒ«áƒ•ელებული áƒáƒ áƒ-isanpnp ბáƒáƒ áƒáƒ—ების ძებნრდრმáƒáƒ“ულების " +"პáƒáƒ áƒáƒ›áƒ”ტრების გáƒáƒ›áƒáƒ¢áƒáƒœáƒ\n" +" -P|--listprobe მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი მáƒáƒ«áƒ•ელებული ბáƒáƒ áƒáƒ—ების მáƒáƒ“ულების ჩáƒáƒ›áƒáƒœáƒáƒ—ვáƒáƒšáƒ˜\n" +" -c|--config ფáƒáƒ˜áƒšáƒ˜\n" +" მáƒáƒ“ულის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ ფáƒáƒ˜áƒšáƒ˜áƒ¡ მითითებáƒ\n" +" -R|--resources ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜ DMA დრIRQ რესურსების ჩáƒáƒ›áƒáƒœáƒáƒ—ვáƒáƒšáƒ˜. მხáƒáƒšáƒáƒ“ " +"გáƒáƒ›áƒáƒ áƒ—ვისთვის\n" +" -h|--help რáƒáƒ¡áƒáƒª კითხულáƒáƒ‘თ" + +#: ../alsaconf.in:254 +msgid "You must be root to use this script." +msgstr "áƒáƒ› სკრიპტის გáƒáƒ›áƒáƒ¡áƒáƒ§áƒ”ნებლáƒáƒ“ root-ის პრივილეგიებირსáƒáƒ­áƒ˜áƒ áƒ." + +#: ../alsaconf.in:298 +msgid "ERROR: The config file doesn't exist: " +msgstr "შეცდáƒáƒ›áƒ: კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ ფáƒáƒ˜áƒšáƒ˜ áƒáƒ  áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს: " + +#: ../alsaconf.in:352 +msgid "Error, dialog or whiptail not found." +msgstr "შეცდáƒáƒ›áƒ: dialog დრwhiptail áƒáƒ¦áƒ›áƒáƒ©áƒ”ნილი áƒáƒ áƒáƒ." + +#: ../alsaconf.in:358 +msgid "Error, awk not found. Can't continue." +msgstr "შეცდáƒáƒ›áƒ: awk დáƒáƒ§áƒ”ნებული áƒáƒ áƒáƒ. გáƒáƒ’რძელებრშეუძლებელიáƒ." + +#: ../alsaconf.in:463 +msgid "" +"\n" +" ALSA CONFIGURATOR\n" +" version %s\n" +"\n" +" This script is a configurator for\n" +" Advanced Linux Sound Architecture (ALSA) driver.\n" +"\n" +"\n" +" If ALSA is already running, you should close all sound\n" +" apps now and stop the sound driver.\n" +" alsaconf will try to do this, but it's not 100%% sure." +msgstr "" +"\n" +" ALSA -ის მáƒáƒ›áƒ áƒ’ები\n" +" ვერსირ%s\n" +"\n" +"\t\t\t\t\tეს სკრიპტი \n" +" Advanced Linux Sound Architecture (ALSA) დრáƒáƒ˜áƒ•ერის მáƒáƒ áƒ’ებისთვისáƒáƒ.\n" +"\n" +"\n" +" თუ ALSA უკვე გáƒáƒ¨áƒ•ებულიáƒ, თქვენ უნდრდáƒáƒ®áƒ£áƒ áƒáƒ— ხმის ყველრáƒáƒžáƒšáƒ˜áƒ™áƒáƒªáƒ˜áƒ დáƒ\n" +" გáƒáƒáƒ©áƒ”რáƒáƒ— ხმის ბáƒáƒ áƒáƒ—ის დრáƒáƒ˜áƒ•ერი.\n" +" alsaconf -ი ცდის, ეს გáƒáƒáƒ™áƒ”თáƒáƒ¡, მáƒáƒ’რáƒáƒ› პრáƒáƒªáƒ”დურრ100%% -ით ყáƒáƒ•ელთვის áƒáƒ  " +"მუშáƒáƒáƒ‘ს." + +#: ../alsaconf.in:479 +msgid "" +"\n" +"\n" +" OK, sound driver is configured.\n" +"\n" +" ALSA CONFIGURATOR\n" +"\n" +" will prepare the card for playing now.\n" +"\n" +" Now I'll run alsasound init script, then I'll use\n" +" amixer to raise the default volumes.\n" +" You can change the volume later via a mixer\n" +" program such as alsamixer or gamix.\n" +" \n" +" " +msgstr "" +"\n" +"\n" +" ხმის ბáƒáƒ áƒáƒ—ის დრáƒáƒ˜áƒ•ერის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ დáƒáƒ¡áƒ áƒ£áƒšáƒ“áƒ.\n" +"\n" +" ALSA -ის მáƒáƒ›áƒ áƒ’ები\n" +"\n" +" მáƒáƒáƒ›áƒ–áƒáƒ“ებს ბáƒáƒ áƒáƒ—ს.\n" +"\n" +" áƒáƒ®áƒšáƒ გáƒáƒ•უშვებ alsasound-ის init-ის სკრიპტს, შემდეგ კი\n" +" amixer-ით ხმის სიძლიერეს ნáƒáƒ’ულისხმებ დáƒáƒœáƒ”ზე დáƒáƒ•áƒáƒ§áƒ”ნებ.\n" +" ხმის სიძლიერის შეცვლრმáƒáƒ’ვიáƒáƒœáƒ”ბით მიქსერის რáƒáƒ›áƒ”ლიმე პრáƒáƒ’რáƒáƒ›áƒ˜áƒ—,\n" +" რáƒáƒ’áƒáƒ áƒ˜áƒªáƒáƒ alsamixer დრgamix, შეგიძლიáƒáƒ—.\n" +" " + +#: ../alsaconf.in:544 ../alsaconf.in:549 ../alsaconf.in:554 ../alsaconf.in:559 +msgid "Can't create temp file, exiting..." +msgstr "დრáƒáƒ”ბითი ფáƒáƒ˜áƒšáƒ”ბის შექმნის შეცდáƒáƒ›áƒ. მუშáƒáƒáƒ‘ის დáƒáƒ¡áƒ áƒ£áƒšáƒ”ბáƒ..." + +#: ../alsaconf.in:669 +msgid "Building card database.." +msgstr "ბáƒáƒ áƒáƒ—ების ბáƒáƒ–ის áƒáƒ’ებáƒ.." + +#: ../alsaconf.in:673 +msgid "No card database is found.." +msgstr "ბáƒáƒ áƒáƒ—ების ბáƒáƒ–რვერ ვიპáƒáƒ•ე.." + +#: ../alsaconf.in:678 +msgid "Searching sound cards" +msgstr "ხმის ბáƒáƒ áƒáƒ—ების ძებნáƒ" + +#: ../alsaconf.in:832 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s (and %s if present)?" +msgstr "" +"\n" +"%s-ის მáƒáƒ áƒ’ებáƒ\n" +"გნებáƒáƒ•თ, შეცვáƒáƒšáƒáƒ— %s (დრ%s, თუ ის áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს)?" + +#: ../alsaconf.in:837 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s?" +msgstr "" +"\n" +"%s-ის მáƒáƒ áƒ’ებáƒ\n" +"გნებáƒáƒ•თ, შეცვáƒáƒšáƒáƒ— %s?" + +#: ../alsaconf.in:928 +msgid "Running update-modules..." +msgstr "\"update-modules\"-ის გáƒáƒ¨áƒ•ებáƒ.." + +#: ../alsaconf.in:943 +msgid "" +"\n" +" The mixer is set up now for for playing.\n" +" Shall I try to play a sound sample now?\n" +"\n" +" NOTE:\n" +"If you have a big amplifier, lower your volumes or say no.\n" +" Otherwise check that your speaker volume is open,\n" +" and look if you can hear test sound.\n" +msgstr "" +"\n" +" მიქსერი áƒáƒ®áƒšáƒ დáƒáƒ™áƒ•რისთვის მზáƒáƒ“áƒáƒ.\n" +" დáƒáƒ•უკრრსáƒáƒ¢áƒ”სტრხმáƒ?\n" +"\n" +" გáƒáƒ¤áƒ áƒ—ხილებáƒ:\n" +"თუ დიდი გáƒáƒ›áƒáƒ«áƒšáƒ˜áƒ”რებელი გáƒáƒ¥áƒ•თ, ჩáƒáƒ£áƒ¬áƒ˜áƒ”თ ხმáƒáƒ¡, áƒáƒœ áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ \"áƒáƒ áƒ\".\n" +" სხვრშემთხვევáƒáƒ¨áƒ˜ შეáƒáƒ›áƒáƒ¬áƒ›áƒ”თ, რáƒáƒ› დინáƒáƒ›áƒ˜áƒ™áƒ¡ ხმრჩáƒáƒ áƒ—ული ჰქáƒáƒœáƒ“ეს\n" +" დრნáƒáƒ®áƒ”თ, გáƒáƒ˜áƒ’ებთ თუ áƒáƒ áƒ ხმáƒáƒ¡.\n" + +#: ../alsaconf.in:961 +msgid "" +"Saving the mixer setup used for this in @ASOUND_STATE_DIR@/asound.state." +msgstr "" +"áƒáƒ›áƒ˜áƒ¡áƒ—ვის გáƒáƒ›áƒáƒ§áƒ”ნებული მიქსერის პáƒáƒ áƒáƒ›áƒ”ტრები @ASOUND_STATE_DIR@/asound.state -" +"ში შეინáƒáƒ®áƒ”ბáƒ." + +#: ../alsaconf.in:965 +msgid "" +"\n" +"===============================================================================\n" +"\n" +" Now ALSA is ready to use.\n" +" For adjustment of volumes, use your favorite mixer.\n" +"\n" +" Have a lot of fun!\n" +"\n" +msgstr "" +"\n" +"===============================================================================\n" +"\n" +"áƒáƒ®áƒšáƒ ALSA გáƒáƒ›áƒáƒ§áƒ”ნებისთვის მზáƒáƒ“áƒáƒ.\n" +"ხმის áƒáƒ¡áƒáƒ¬áƒ”ვ ჩáƒáƒ¡áƒáƒ¬áƒ”ვáƒáƒ“ თქვენი სáƒáƒ§áƒ•áƒáƒ áƒ”ლი მიქსერის გáƒáƒ›áƒáƒ§áƒ”ნებრშეგიძლიáƒáƒ—.\n" +"\n" +"გáƒáƒ”რთეთ!\n" +"\n" + +#: ../alsaconf.in:1273 +msgid "WARNING" +msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ" + +#: ../alsaconf.in:1274 +msgid "" +"\n" +" Probing legacy ISA cards might make\n" +" your system unstable.\n" +"\n" +" Do you want to proceed?\n" +"\n" +msgstr "" +"\n" +" მიმდინáƒáƒ áƒ”áƒáƒ‘ს მáƒáƒ«áƒ•ელებული ISA ბáƒáƒ áƒáƒ—ების ძებნáƒ. áƒáƒ›áƒáƒœ\n" +" თქვენი სისტემრáƒáƒ áƒáƒ¡áƒ¢áƒáƒ‘ილური შეიძლებრგáƒáƒ®áƒáƒ“áƒáƒ¡.\n" +"\n" +" გნებáƒáƒ•თ, გáƒáƒáƒ’რძელáƒáƒ—?\n" +"\n" + +#: ../alsaconf.in:1297 +msgid "" +"No legacy drivers are available\n" +" for your machine" +msgstr "" +"თქვენი მáƒáƒœáƒ¥áƒáƒœáƒ˜áƒ¡áƒ—ვის მáƒáƒ«áƒ•ელებული\n" +"დრáƒáƒ˜áƒ•ერები áƒáƒ¦áƒ›áƒáƒ©áƒ”ნილი áƒáƒ áƒáƒ" + +#: ../alsaconf.in:1302 +msgid "Driver Selection" +msgstr "დრáƒáƒ˜áƒ•ერის áƒáƒ áƒ©áƒ”ვáƒáƒœáƒ˜" + +#: ../alsaconf.in:1303 +msgid "" +" Probing legacy ISA cards\n" +"\n" +" Please select the drivers to probe:" +msgstr "" +" მáƒáƒ«áƒ•ელებული ISA ბáƒáƒ áƒáƒ—ების ძებნáƒ\n" +"\n" +" áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ჩáƒáƒ¡áƒáƒ¢áƒ•ირთი დრáƒáƒ˜áƒ•ერები:" + +#: ../alsaconf.in:1310 +msgid "" +"\n" +" Shall I try all possible DMA and IRQ combinations?\n" +" With this option, some unconventional configuration\n" +" might be found, but it will take much longer time." +msgstr "" +"\n" +"ვცáƒáƒ“რDMA/IRQ-ის ყველრშესáƒáƒ«áƒšáƒ კáƒáƒ›áƒ‘ინáƒáƒªáƒ˜áƒ?\n" +"áƒáƒ› პáƒáƒ áƒáƒ›áƒ”ტრით შეიძლებრუჩვეულრკáƒáƒ›áƒ‘ინáƒáƒªáƒ˜áƒ”ბიც áƒáƒ¦áƒ›áƒáƒáƒ©áƒœáƒ˜áƒœáƒáƒ—\n" +"მáƒáƒ’რáƒáƒ› მáƒáƒ¡ გáƒáƒªáƒ˜áƒšáƒ”ბით მეტი დრრსჭირდებáƒ." + +#: ../alsaconf.in:1320 +msgid "Probing legacy cards.. This may take a few minutes.." +msgstr "" +"მáƒáƒ«áƒ•ელებული ხმის ბáƒáƒ áƒáƒ—ების ძებნáƒ. áƒáƒ›áƒáƒ¡ რáƒáƒ›áƒ“ენიმე წუთი შეიძლებრ" +"დáƒáƒ¡áƒ­áƒ˜áƒ áƒ“ეს.." + +#: ../alsaconf.in:1321 +msgid "Probing: " +msgstr "ძებნáƒ: " + +#: ../alsaconf.in:1327 +msgid " : FOUND!!" +msgstr " : ვიპáƒáƒ•ე!!" + +#: ../alsaconf.in:1333 +msgid "Result" +msgstr "შედეგი" + +#: ../alsaconf.in:1334 +msgid "No legacy cards found" +msgstr "მáƒáƒ«áƒ•ელებული ბáƒáƒ áƒáƒ—ები ვერ ვიპáƒáƒ•ე" + +#: ../alsaconf.in:1393 +msgid "" +"\n" +" Looks like you having a Thinkpad 600E or 770 notebook.\n" +" On this notebook, CS4236 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4236 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +"რáƒáƒ’áƒáƒ áƒª ჩáƒáƒœáƒ¡, თქვენ Thinkpad 600E áƒáƒœ 770 ნáƒáƒ£áƒ—ბუქი გáƒáƒ¥áƒ•თ.\n" +"áƒáƒ› ნáƒáƒ£áƒ—ბუქზე CS46xx ჩიპის მáƒáƒ’იერ CS4236 დრáƒáƒ˜áƒ•ერის გáƒáƒ›áƒáƒ§áƒ”ნებáƒáƒ სáƒáƒ­áƒ˜áƒ áƒ\n" +"\n" +"ვცáƒáƒ“რsnd-cs4236 დრáƒáƒ˜áƒ•ერი დრმáƒáƒ•ძებნáƒ\n" +"მáƒáƒ«áƒ•ელებული ICS კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ?" + +#: ../alsaconf.in:1407 +msgid "" +"\n" +" Looks like you having a Dell Dimension machine.\n" +" On this machine, CS4232 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4232 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +"რáƒáƒ’áƒáƒ áƒª ჩáƒáƒœáƒ¡, თქვენ Dell Dimension მáƒáƒœáƒ¥áƒáƒœáƒ გáƒáƒ¥áƒ•თ.\n" +"áƒáƒ› მáƒáƒœáƒ¥áƒáƒœáƒáƒ–ე CS46xx ჩიპის მáƒáƒ’იერ CS4232 დრáƒáƒ˜áƒ•ერის გáƒáƒ›áƒáƒ§áƒ”ნებáƒáƒ სáƒáƒ­áƒ˜áƒ áƒ\n" +"\n" +"ვცáƒáƒ“რsnd-cs4232 დრáƒáƒ˜áƒ•ერი დრმáƒáƒ•ძებნáƒ\n" +"მáƒáƒ«áƒ•ელებული ICS კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ?" + +#: ../alsaconf.in:1424 +msgid "Soundcard Selection" +msgstr "ხმის ბáƒáƒ áƒáƒ—ის áƒáƒ áƒ©áƒ”ვáƒáƒœáƒ˜" + +#: ../alsaconf.in:1425 +msgid "" +"\n" +" Following card(s) are found on your system.\n" +" Choose a soundcard to configure:\n" +msgstr "" +"\n" +" თქვენს სისტემáƒáƒ¨áƒ˜ შემდეგი ხმის ბáƒáƒ áƒáƒ—ებირნáƒáƒžáƒáƒ•ნი.\n" +" áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ მáƒáƒ¡áƒáƒ áƒ’ები ხმის ბáƒáƒ áƒáƒ—ი:\n" + +#: ../alsaconf.in:1438 +msgid "" +"\n" +" No supported PnP or PCI card found.\n" +"\n" +" Would you like to probe legacy ISA sound cards/chips?\n" +"\n" +msgstr "" +"\n" +" მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი PnP დრPCI ბáƒáƒ áƒáƒ—ები áƒáƒ¦áƒ›áƒáƒ©áƒ”ნილი áƒáƒ áƒáƒ.\n" +"\n" +"გნებáƒáƒ•თ მáƒáƒ«áƒ•ელებული ISA ხმáƒáƒ•áƒáƒœáƒ˜ ბáƒáƒ áƒáƒ—ების/ჩიპების მáƒáƒ«áƒ”ბნáƒ?\n" +"\n" diff --git a/alsaconf/po/ru.po b/alsaconf/po/ru.po new file mode 100644 index 0000000..af4fa0d --- /dev/null +++ b/alsaconf/po/ru.po @@ -0,0 +1,373 @@ +# Russian translations for alsa-utils package +# ÐнглийÑкие переводы Ð´Ð»Ñ Ð¿Ð°ÐºÐµÑ‚Ð° alsa-utils. +# Copyright (C) 2005 THE alsa-utils'S COPYRIGHT HOLDER +# This file is distributed under the same license as the alsa-utils package. +# , 2005. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.9rc4a\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2005-12-02 12:37+0100\n" +"PO-Revision-Date: 2005-12-02 12:39+0100\n" +"Last-Translator: \n" +"Language-Team: Russian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%" +"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: ../alsaconf.in:36 +msgid "-s" +msgstr "" + +#: ../alsaconf.in:80 +msgid "ALSA configurator" +msgstr "ÐайÑтройщик ALSA" + +#: ../alsaconf.in:82 +#, fuzzy +msgid "" +"usage: alsaconf [options]\n" +" -l|--legacy check only legacy non-isapnp cards\n" +" -m|--modinfo read module descriptions instead of reading card db\n" +" -s|--sound wav-file\n" +" use the specified wav file as a test sound\n" +" -u|--uid uid set the uid for the ALSA devices (default = 0) [obsoleted]\n" +" -g|--gid gid set the gid for the ALSA devices (default = 0) [obsoleted]\n" +" -d|--devmode mode\n" +" set the permission for ALSA devices (default = 0666) " +"[obs.]\n" +" -r|--strict set strict device mode (equiv. with -g 17 -d 0660) " +"[obsoleted]\n" +" -L|--log file logging on the specified file (for debugging purpose only)\n" +" -p|--probe card-name\n" +" probe a legacy non-isapnp card and print module options\n" +" -P|--listprobe list the supported legacy card modules\n" +" -c|--config file\n" +" specify the module config file\n" +" -R|--resources list available DMA and IRQ resources with debug for legacy\n" +" -h|--help what you're reading" +msgstr "" +"иÑпользование: alsaconf [опции]\n" +" -l|--legacy проверÑть только не ISAPnP карты\n" +" -m|--modinfo читать опиÑание модулей вмеÑто Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð·Ñ‹ данных карт\n" +" -s|--sound WAV файл\n" +" иÑпользовать указанный WAV файл в качеÑтве теÑтового\n" +" -u|--uid uid уÑтанавливает uid Ð´Ð»Ñ ALSA(по умолчанию = 0) [уÑтарело]\n" +" -g|--gid gid уÑтанавливает gid Ð´Ð»Ñ ALSA(по умолчанию = 0) [уÑтарело]\n" +" -d|--devmode режим\n" +" уÑтанавливает права доÑтупа Ð´Ð»Ñ ALSA (по умолчанию = 0666) " +"[уÑÑ‚.]\n" +" -r|--strict уÑтанавливает Ñтрогие права Ð´Ð»Ñ ALSA (Ñкв. -g 17 -d 0660) " +"[уÑÑ‚.]\n" +" -L|--log файл запиÑывать ÑÐ¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð² определенный файл (в целÑÑ… отладки)\n" +" -p|--probe Ð¸Ð¼Ñ Ð·Ð²ÑƒÐºÐ¾Ð²Ð¾Ð¹ карты\n" +" проверить обычную не IsaPnP карту и вывеÑти опции модулÑ\n" +" -P|--listprobe вывеÑти ÑпиÑок поддерживаемых не IsaPnP карт\n" +" -c|--config файл\n" +" указывает файл Ñ Ð½Ð°Ñтройками модулÑ\n" +" -h|--help вывеÑти Ñто Ñообщение" + +#: ../alsaconf.in:252 +msgid "You must be root to use this script." +msgstr "Ð’Ñ‹ должны быть Ñуперпользователем, чтобы иÑпользовать Ñтот Ñкрипт." + +#: ../alsaconf.in:294 +msgid "ERROR: The config file doesn't exist: " +msgstr "ОШИБКÐ: Файл Ñ Ð½Ð°Ñтройками не ÑущеÑтвует: " + +#: ../alsaconf.in:336 +msgid "Error, dialog or whiptail not found." +msgstr "Ошибка, dialog или whiptail не найдены." + +#: ../alsaconf.in:342 +msgid "Error, awk not found. Can't continue." +msgstr "Ошибка, awk не найден. Ðе могу продолжить работу." + +#: ../alsaconf.in:439 +msgid "" +"\n" +" ALSA CONFIGURATOR\n" +" version %s\n" +"\n" +" This script is a configurator for\n" +" Advanced Linux Sound Architecture (ALSA) driver.\n" +"\n" +"\n" +" If ALSA is already running, you should close all sound\n" +" apps now and stop the sound driver.\n" +" alsaconf will try to do this, but it's not 100%% sure." +msgstr "" +"\n" +" ÐаÑтройщик ALSA\n" +" верÑÐ¸Ñ %s\n" +"\n" +" Этот Ñкрипт наÑтраивает Advanced Linux Sound Architecture\n" +"\n" +"\n" +" ЕÑли какие-нибудь Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð²Ñ…Ð¾Ð´Ñщие в пакет ALSA уже запущены,\n" +" вы должны закрыть вÑе Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð°ÑŽÑ‰Ð¸Ðµ Ñо звуком\n" +" и оÑтановаить звуковой драйвер.\n" +" alsaconf попытаетÑÑ Ñто Ñделать, но результат не гарантируетÑÑ Ð½Ð° вÑе 100%" +"%." + +#: ../alsaconf.in:455 +msgid "" +"\n" +"\n" +" OK, sound driver is configured.\n" +"\n" +" ALSA CONFIGURATOR\n" +"\n" +" will prepare the card for playing now.\n" +"\n" +" Now I'll run alsasound init script, then I'll use\n" +" amixer to raise the default volumes.\n" +" You can change the volume later via a mixer\n" +" program such as alsamixer or gamix.\n" +" \n" +" " +msgstr "" +"\n" +"\n" +" Хорошо, драйвер звуковой карты наÑтроен.\n" +"\n" +" Ð¡ÐµÐ¹Ñ‡Ð°Ñ ÐаÑтройщик ALSA\n" +"\n" +" подготовит звуковую карту Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð¸Ð³Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ.\n" +"\n" +" Ð¡ÐµÐ¹Ñ‡Ð°Ñ Ñ Ð·Ð°Ð¿ÑƒÑ‰Ñƒ Ñкрипт, инициализирующий alsasound, потом иÑпользую " +"amixer\n" +" Ð´Ð»Ñ ÑƒÐ²ÐµÐ»Ð¸Ñ‡ÐµÐ½Ð¸Ñ ÑƒÑ€Ð¾Ð²Ð½Ñ Ð³Ñ€Ð¾Ð¼ÐºÐ¾Ñти по умолчаниÑ.\n" +" Ð’Ñ‹ можете изменить уровень громкоÑти по ÑƒÐ¼Ð¾Ð»Ñ‡Ð°Ð½Ð¸Ñ Ð¿Ð¾Ð·Ð¶Ðµ\n" +" Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ таких программ как alsamixer или gamix.\n" +" " + +#: ../alsaconf.in:518 ../alsaconf.in:523 ../alsaconf.in:528 ../alsaconf.in:533 +msgid "Can't create temp file, exiting..." +msgstr "Ðе могу Ñоздать временный файл, прекращаю работу..." + +#: ../alsaconf.in:643 +msgid "Building card database.." +msgstr "Создаю базу данных Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸ÐµÐ¹ о звуковых карточках.." + +#: ../alsaconf.in:647 +msgid "No card database is found.." +msgstr "База данных, ÑÐ¾Ð´ÐµÑ€Ð¶Ð°Ñ‰Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸ÑŽ о звуковых карточках, не найдена.." + +#: ../alsaconf.in:652 +msgid "Searching sound cards" +msgstr "Ищем звуковые карточки" + +#: ../alsaconf.in:806 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s (and %s if present)?" +msgstr "" +"\n" +"ÐаÑтраиваем %s\n" +"Ð’Ñ‹ хотите изменить %s (и %s, еÑли ÑущеÑтвует)?" + +#: ../alsaconf.in:811 +msgid "" +"\n" +"Configuring %s\n" +"Do you want to modify %s?" +msgstr "" +"\n" +"ÐаÑтраиваем %s\n" +"Ð’Ñ‹ хотите изменить %s?" + +#: ../alsaconf.in:900 +msgid "Running modules-update..." +msgstr "ЗапуÑкаем modules-update..." + +#: ../alsaconf.in:903 +msgid "Running update-modules..." +msgstr "ЗапуÑкаем update-modules..." + +#: ../alsaconf.in:915 +msgid "" +"\n" +" The mixer is set up now for for playing.\n" +" Shall I try to play a sound sample now?\n" +"\n" +" NOTE:\n" +"If you have a big amplifier, lower your volumes or say no.\n" +" Otherwise check that your speaker volume is open,\n" +" and look if you can hear test sound.\n" +msgstr "" +"\n" +" Микшер наÑтроен Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð¸Ð³Ñ€Ð¾Ð²Ð¾Ð²Ð°Ð½Ð¸Ñ.\n" +" Проиграть теÑтовый пример?\n" +"\n" +" Примечание:\n" +" ЕÑли у Ð²Ð°Ñ Ð±Ð¾Ð»ÑŒÑˆÐ¾Ð¹ уÑилитель, уменьшите уровень громкоÑти или Ñкажите " +"нет.\n" +" Ð’ противном Ñлучае проверьте, что ваши колонки включены,\n" +" и удоÑтоверьтеÑÑŒ что вы Ñлышите теÑтовый пример.\n" + +#: ../alsaconf.in:932 +msgid "Saving the mixer setup used for this in /etc/asound.state." +msgstr "СохранÑем наÑтройки микшера в /etc/asound.state." + +#: ../alsaconf.in:936 +msgid "" +"\n" +"===============================================================================\n" +"\n" +" Now ALSA is ready to use.\n" +" For adjustment of volumes, use your favorite mixer.\n" +"\n" +" Have a lot of fun!\n" +"\n" +msgstr "" +"\n" +"===============================================================================\n" +"\n" +" Теперь ALSA готова к иÑпользованию.\n" +" Ð”Ð»Ñ Ñ€ÐµÐ³ÑƒÐ»Ð¸Ñ€Ð¾Ð²ÐºÐ¸ ÑƒÑ€Ð¾Ð²Ð½Ñ Ð³Ñ€Ð¾Ð¼ÐºÐ¾Ñти, иÑпользуйте ваш любимый микшер.\n" +"\n" +" ОторвиÑÑŒ по полной!\n" +"\n" + +#: ../alsaconf.in:1244 +msgid "WARNING" +msgstr "ПРЕДУПРЕЖДЕÐИЕ" + +#: ../alsaconf.in:1245 +msgid "" +"\n" +" Probing legacy ISA cards might make\n" +" your system unstable.\n" +"\n" +" Do you want to proceed?\n" +"\n" +msgstr "" +"\n" +" ПоиÑк ISA звуковых карточек может Ñделать\n" +" вашу ÑиÑтему неÑтабильной.\n" +"\n" +" Ð’Ñ‹ уверены?\n" +"\n" + +#: ../alsaconf.in:1268 +msgid "" +"No legacy drivers are available\n" +" for your machine" +msgstr "" +"Ðету подходÑшего драйвера длÑ\n" +" вашей машины" + +#: ../alsaconf.in:1273 +msgid "Driver Selection" +msgstr "Выбор Драйвера" + +#: ../alsaconf.in:1274 +msgid "" +" Probing legacy ISA cards\n" +"\n" +" Please select the drivers to probe:" +msgstr "" +" ПоиÑк ISA звуковых карт\n" +"\n" +" ПожалуйÑта, выберите драйверы Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸:" + +#: ../alsaconf.in:1281 +msgid "" +"\n" +" Shall I try all possible DMA and IRQ combinations?\n" +" With this option, some unconventional configuration\n" +" might be found, but it will take much longer time." +msgstr "" +"\n" +" Попробовать вÑе доÑтупные DMA и IRQ комбинации?\n" +" С Ñтой опцией, могут быть найдены некоторые неÑтандартные\n" +" конфигураии, но Ñто займет намного больше времени." + +#: ../alsaconf.in:1291 +msgid "Probing legacy cards.. This may take a few minutes.." +msgstr "ПоиÑк звуковых карточек.. Это может занÑть неÑколько минут.." + +#: ../alsaconf.in:1292 +msgid "Probing: " +msgstr "Проверка: " + +#: ../alsaconf.in:1298 +msgid " : FOUND!!" +msgstr " : ÐÐШЕЛ!!" + +#: ../alsaconf.in:1304 +msgid "Result" +msgstr "Результат" + +#: ../alsaconf.in:1305 +msgid "No legacy cards found" +msgstr "Звуковых карточек не найдено" + +#: ../alsaconf.in:1364 +msgid "" +"\n" +" Looks like you having a Thinkpad 600E or 770 notebook.\n" +" On this notebook, CS4236 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4236 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +" КажетÑÑ, мы имеем дело Ñ Ð½Ð¾ÑƒÑ‚Ð±ÑƒÐºÐ¾Ð¼ Thinkpad 600E или 770.\n" +" Ðа Ñтом ноутбуке, драйвер CS4236 должен иÑпользоватьÑÑ\n" +" Ñ…Ð¾Ñ‚Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÑетÑÑ CS46xx\n" +"\n" +" ИÑпользовать snd-cs4236 и проверить правильноÑть ISA\n" +" конфигурации?" + +#: ../alsaconf.in:1378 +msgid "" +"\n" +" Looks like you having a Dell Dimension machine.\n" +" On this machine, CS4232 driver should be used\n" +" although CS46xx chip is detected.\n" +"\n" +" Shall I try to snd-cs4232 driver and probe\n" +" the legacy ISA configuration?" +msgstr "" +"\n" +" Похоже что вы имеем дело Ñ Ð¼Ð°ÑˆÐ¸Ð½Ð¾Ð¹ Dell Dimension.\n" +" Ðа Ñтой машине CS4232 драйвер должен иÑползьовать, ãƒãƒƒãƒ—ãŒæ¤œå‡ºã•れã¾ã™ãŒ\n" +" Ñ…Ð¾Ñ‚Ñ Ñ‡Ð¸Ð¿ звуковой карты определÑетÑÑ ÐºÐ°Ðº CS46xx.\n" +"\n" +" ИÑпользовать snd-cs4232 и проверить ISA валидноÑть\n" +" Ñтой конфигурации?" + +#: ../alsaconf.in:1395 +msgid "Soundcard Selection" +msgstr "Выбор Звуковой карты" + +#: ../alsaconf.in:1396 +msgid "" +"\n" +" Following card(s) are found on your system.\n" +" Choose a soundcard to configure:\n" +msgstr "" +"\n" +" Следущее звуковаÑ(ые) карта(Ñ‹) найдены в вашей ÑиÑтеме.\n" +" Выберите звуковую карту Ð´Ð»Ñ Ð½Ð°Ñтройки:\n" + +#: ../alsaconf.in:1409 +msgid "" +"\n" +" No supported PnP or PCI card found.\n" +"\n" +" Would you like to probe legacy ISA sound cards/chips?\n" +"\n" +msgstr "" +"\n" +" Ðайдена не Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶Ð¸Ð²Ð°ÐµÐ¼Ð°Ñ PnP или PCI карта.\n" +"\n" +" Желаете проверить ISA звуковые карты/чипы?\n" +"\n" diff --git a/alsactl/.gitignore b/alsactl/.gitignore new file mode 100644 index 0000000..56ab3a2 --- /dev/null +++ b/alsactl/.gitignore @@ -0,0 +1,3 @@ +alsa-store.service +alsa-restore.service +90-alsa-restore.rules diff --git a/alsactl/90-alsa-restore.rules.in b/alsactl/90-alsa-restore.rules.in new file mode 100644 index 0000000..5db6080 --- /dev/null +++ b/alsactl/90-alsa-restore.rules.in @@ -0,0 +1,30 @@ +# do not edit this file, it will be overwritten on update + +ACTION=="add", SUBSYSTEM=="sound", KERNEL=="controlC*", KERNELS!="card*",@extratest@ GOTO="alsa_restore_go" +GOTO="alsa_restore_end" + +LABEL="alsa_restore_go" + +ENV{ALSA_CARD_NUMBER}="$attr{device/number}" + +# mark HDA analog card; HDMI/DP card does not have capture devices +DRIVERS=="snd_hda_intel", TEST=="device/pcmC$env{ALSA_CARD_NUMBER}D0p", RUN+="/bin/sh -c 'echo ALSA_CARD_HDA_ANALOG=$env{ALSA_CARD_NUMBER} >> /run/udev/alsa-hda-analog-card'" + +# check for ACP hardware +TEST=="device/device/acp3x-dmic-capture", GOTO="alsa_hda_analog" +TEST=="device/device/acp6x-dmic-capture", GOTO="alsa_hda_analog" +TEST=="device/device/acp63-dmic-capture", GOTO="alsa_hda_analog" +TEST=="device/device/acp-dmic-codec", GOTO="alsa_hda_analog" +GOTO="alsa_restore_std" + +LABEL="alsa_hda_analog" +# restore configuration for profile with combined cards (HDA + digital mic) +TEST!="/run/udev/alsa-hda-analog-card", GOTO="alsa_restore_std" +IMPORT{program}="/usr/bin/cat /run/udev/alsa-hda-analog-card" +ENV{ALSA_CARD_HDA_ANALOG}!="", ENV{ALSA_CARD_NUMBER}="$env{ALSA_CARD_HDA_ANALOG}" + +LABEL="alsa_restore_std" +TEST!="@daemonswitch@", RUN+="@sbindir@/alsactl@args@ restore $env{ALSA_CARD_NUMBER}" +TEST=="@daemonswitch@", RUN+="@sbindir@/alsactl@args@ nrestore $env{ALSA_CARD_NUMBER}" + +LABEL="alsa_restore_end" diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am new file mode 100644 index 0000000..e771717 --- /dev/null +++ b/alsactl/Makefile.am @@ -0,0 +1,76 @@ +SUBDIRS = init + +sbin_PROGRAMS=alsactl +man_MANS=alsactl.1 +if USE_XMLTO +man_MANS += alsactl_init.7 +endif +EXTRA_DIST=alsactl.1 alsactl_init.xml + +AM_CFLAGS = -D_GNU_SOURCE + +AM_CPPFLAGS = -I$(top_srcdir)/include + +alsactl_SOURCES=alsactl.c state.c lock.c utils.c init_parse.c init_ucm.c \ + daemon.c monitor.c clean.c info.c + +alsactl_CFLAGS=$(AM_CFLAGS) -D__USE_GNU \ + -DSYS_ASOUNDRC=\"$(ASOUND_STATE_DIR)/asound.state\" \ + -DSYS_LOCKPATH=\"$(ASOUND_LOCK_DIR)\" \ + -DSYS_LOCKFILE=\"asound.state.lock\" \ + -DSYS_PIDFILE=\"$(ALSACTL_PIDFILE_DIR)/alsactl.pid\" + +noinst_HEADERS=alsactl.h list.h init_sysdeps.c init_utils_string.c \ + init_utils_run.c init_sysfs.c + +udevrules_DATA = \ + 90-alsa-restore.rules + +if HAVE_SYSTEMD + +systemdsystemunit_DATA = \ + alsa-state.service \ + alsa-restore.service + +install-data-hook: + $(MKDIR_P) -m 0755 \ + $(DESTDIR)$(systemdsystemunitdir)/sound.target.wants + ( cd $(DESTDIR)$(systemdsystemunitdir)/sound.target.wants && \ + rm -f alsa-state.service alsa-restore.service && \ + $(LN_S) ../alsa-state.service alsa-state.service && \ + $(LN_S) ../alsa-restore.service alsa-restore.service) + +endif + +edit = \ + extratest=$$(echo ' $(ALSACTL_UDEV_EXTRATEST)' | sed -e 's/__/ /g' -e 's/^ $$//'); \ + args=$$(echo ' $(ALSACTL_UDEV_ARGS)' | sed -e 's/__/ /g' -e 's/^ $$//'); \ + $(SED) -r -e 's,@sbindir\@,$(sbindir),g' \ + -e 's,@mydatadir\@,$(mydatadir),g' \ + -e 's,@daemonswitch\@,$(ALSACTL_DAEMONSWITCH),g' \ + -e 's,@asoundrcfile\@,$(ASOUND_STATE_DIR)/asound.state,g' \ + -e "s;@extratest\@;$${extratest};g" \ + -e "s;@args\@;$${args};g" \ + < $< > $@ || rm $@ + +alsa-state.service: alsa-state.service.in + $(edit) + +alsa-restore.service: alsa-restore.service.in + $(edit) + +90-alsa-restore.rules: 90-alsa-restore.rules.in + $(edit) + +EXTRA_DIST += \ + alsa-state.service.in \ + alsa-restore.service.in \ + 90-alsa-restore.rules.in + +CLEANFILES = \ + alsa-state.service \ + alsa-restore.service \ + 90-alsa-restore.rules + +%.7: %.xml + xmlto man $? diff --git a/alsactl/alsa-restore.service.in b/alsactl/alsa-restore.service.in new file mode 100644 index 0000000..80fd5fd --- /dev/null +++ b/alsactl/alsa-restore.service.in @@ -0,0 +1,15 @@ +# +# Note that two different ALSA card state management schemes exist and they +# can be switched using a file exist check - /etc/alsa/state-daemon.conf . +# + +[Unit] +Description=Save/Restore Sound Card State +ConditionPathExists=!@daemonswitch@ +ConditionPathExistsGlob=/dev/snd/control* + +[Service] +Type=oneshot +RemainAfterExit=true +ExecStart=-@sbindir@/alsactl restore +ExecStop=-@sbindir@/alsactl store diff --git a/alsactl/alsa-state.service.in b/alsactl/alsa-state.service.in new file mode 100644 index 0000000..5a8fe5e --- /dev/null +++ b/alsactl/alsa-state.service.in @@ -0,0 +1,13 @@ +# +# Note that two different ALSA card state management schemes exist and they +# can be switched using a file exist check - /etc/alsa/state-daemon.conf . +# + +[Unit] +Description=Manage Sound Card State (restore and store) +ConditionPathExists=@daemonswitch@ + +[Service] +Type=simple +ExecStart=-@sbindir@/alsactl -s -n 19 -c rdaemon +ExecStop=-@sbindir@/alsactl -s kill save_and_quit diff --git a/alsactl/alsactl.1 b/alsactl/alsactl.1 new file mode 100644 index 0000000..29da65f --- /dev/null +++ b/alsactl/alsactl.1 @@ -0,0 +1,231 @@ +.TH ALSACTL 1 "07 May 2014" +.SH NAME +alsactl \- advanced controls for ALSA soundcard driver + +.SH SYNOPSIS + +\fBalsactl\fP [\fIoptions\fP] [\fIstore\fP|\fIrestore\fP|\fIinit\fP] + +\fBalsactl\fP \fImonitor\fP + +\fBalsactl\fP \fIinfo\fP + +\fBalsactl\fP [\fIclean\fP] [[control identifiers]] + +.SH DESCRIPTION +\fBalsactl\fP is used to control advanced settings for the ALSA +soundcard drivers. It supports multiple soundcards. If your card has +features that you can't seem to control from a mixer application, +you have come to the right place. + +.SH COMMANDS + +.SS Introduction + +The \fI\fP argument is optional. If no soundcards are specified, +setup for all cards will be saved, loaded or monitored. + +.SS store + +This command saves the current driver state for the selected soundcard +to the configuration file. + +.SS restore + +This command loads driver state for the selected soundcard from the +configuration file. If restoring fails (eventually partly), the init +action is called. + +.SS nrestore + +This command is like \fIrestore\fP, but it notifies also the daemon +to do new rescan for available soundcards. + +.SS init + +This command tries to initialize all devices to a default state. If device +is not known, error code 99 is returned. + +.SS daemon + +This command manages to save periodically the sound state. + +.SS rdaemon + +This command is like \fIdaemon\fP but restore the sound state at first. + +.SS kill + +This command notifies the daemon to do the specified operation (quit, +rescan, save_and_quit). + +.SS monitor + +This command is for monitoring the events received from the given +control device. + +.SS info + +This command shows the general information in the YAML format +collected from the given control device (sound card). + +.SS clean [filter] + +This command cleans the controls created by applications. + +The optional element identifiers are accepted as a filter. One extra +argument is parsed as an element identifiers. + +\fIExample:\fP alsactl clean 0 "name='PCM'" "name='Mic Phantom'" + +.SS dump-state + +This command dumps the current state (all cards) to stdout. + +.SS dump-cfg + +This command dumps the current configuration (all cards) to stdout. +Note that the configuration hooks are evaluated. + +.SH OPTIONS + +.TP +\fI\-h, \-\-help\fP +Help: show available flags and commands. + +.TP +\fI\-d, \-\-debug\fP +Use debug mode: a bit more verbose. + +.TP +\fI\-v, \-\-version\fP +Print alsactl version number. + +.TP +\fI\-f, \-\-file\fP +Select the configuration file to use. The default is /var/lib/alsa/asound.state. + +.TP +\fI\-a, \-\-config-dir\fP +Select the boot / hotplug ALSA configuration directory to use. The default is /var/lib/alsa. + +.TP +\fI\-l, \-\-lock\fP +Use the file locking to serialize the concurrent access to the state file (this +option is default for the global state file). + +.TP +\fI\-L, \-\-no-lock\fP +Do not use the file locking to serialize the concurrent access to the state +file (including the global state file). + +.TP +\fI\-O, \-\-lock-state-file\fP +Select the state lock file path. + +.TP +\fI\-F, \-\-force\fP +Used with restore command. Try to restore the matching control elements +as much as possible. This option is set as default now. + +.TP +\fI\-g, \-\-ignore\fP +Used with store, restore and init commands. Do not show 'No soundcards found' +and do not set an error exit code when soundcards are not installed. + +.TP +\fI\-P, \-\-pedantic\fP +Used with restore command. Don't restore mismatching control elements. +This option was the old default behavior. + +.TP +\fI\-I, \-\-no\-init\-fallback\fP +Don't initialize cards if restore fails. Since version 1.0.18, +\fBalsactl\fP tries to initialize the card with the restore operation +as default. But this can cause incompatibility with the older version. +The caller may expect that the state won't be touched if no state file +exists. This option takes the restore behavior back to the older +version by suppressing the initialization. + +.TP +\fI\-r, \-\-runstate\fP +Save restore and init state to this file. The file will contain only errors. +Errors are appended with the soundcard id to the end of file. + +.TP +\fI\-R, \-\-remove\fP +Remove runstate file at first. + +.TP +\fI\-E, \-\-env\fP #=# +Set environment variable (useful for init action or you may override +ALSA_CONFIG_PATH to read different or optimized configuration - may be +useful for "boot" scripts). + +.TP +\fI\-i, \-\-initfile\fP +The configuration file for init. By default, PREFIX/share/alsa/init/00main +is used. + +.TP +\fI\-p, \-\-period\fP +The store period in seconds for the daemon command. + +.TP +\fI\-e, \-\-pid-file\fP +The pathname to store the process-id file in the HDB UUCP format (ASCII). + +.TP +\fI\-b, \-\-background\fP +Run the task in background. + +.TP +\fI\-s, \-\-syslog\fP +Use syslog for messages. + +.TP +\fI\-n, \-\-nice\fP +Set the process priority (see 'man nice') + +.TP +\fI\-c, \-\-sched-idle\fP +Set the process scheduling policy to idle (SCHED_IDLE). + +.TP +\fI\-D, \-\-ucm-defaults\fP +Execute also the 'defaults' section from the UCM configuration. The standard +behaviour is to execute only 'once' section. + +.TP +\fI\-U, \-\-no-ucm\fP +Skip the UCM init even if available. It may be useful for the test the +legacy init configuration. + +.SH FILES +\fI/var/lib/alsa/asound.state\fP (or whatever file you specify with the +\fB\-f\fP flag) is used to store current settings for your +soundcards. The settings include all the usual soundcard mixer +settings. More importantly, alsactl is +capable of controlling other card-specific features that mixer apps +usually don't know about. + +The configuration file is generated automatically by running +\fBalsactl store\fP. Editing the configuration file by hand may be +necessary for some soundcard features (e.g. enabling/disabling +automatic mic gain, digital output, joystick/game ports, some future MIDI +routing options, etc). + +.SH SEE ALSO +\fB +amixer(1), +alsamixer(1), +aplay(1), +alsactl_init(7) +\fP + +.SH BUGS +None known. + +.SH AUTHOR +\fBalsactl\fP is by Jaroslav Kysela and Abramo Bagnara +. This document is by Paul Winkler . diff --git a/alsactl/alsactl.c b/alsactl/alsactl.c new file mode 100644 index 0000000..8ad991a --- /dev/null +++ b/alsactl/alsactl.c @@ -0,0 +1,495 @@ +/* + * Advanced Linux Sound Architecture Control Program + * Copyright (c) by Abramo Bagnara + * Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include "alsactl.h" +#include "os_compat.h" + +#ifndef SYS_ASOUND_DIR +#define SYS_ASOUND_DIR "/var/lib/alsa" +#endif +#ifndef SYS_ASOUNDRC +#define SYS_ASOUNDRC SYS_ASOUND_DIR "/asound.state" +#endif +#ifndef SYS_PIDFILE +#define SYS_PIDFILE "/var/run/alsactl.pid" +#endif +#ifndef SYS_LOCKPATH +#define SYS_LOCKPATH "/var/lock" +#endif + +int debugflag = 0; +int force_restore = 1; +int ignore_nocards = 0; +int do_lock = 0; +int use_syslog = 0; +char *command; +char *statefile = NULL; +char *lockpath = SYS_LOCKPATH; +char *lockfile = SYS_LOCKFILE; + +#define TITLE 0x0100 +#define HEADER 0x0200 +#define FILEARG 0x0400 +#define ENVARG 0x0800 +#define INTARG 0x1000 +#define EMPCMD 0x2000 +#define CARDCMD 0x4000 +#define KILLCMD 0x8000 + +struct arg { + int sarg; + char *larg; + char *comment; +}; + +static struct arg args[] = { +{ TITLE, NULL, "Usage: alsactl command" }, +{ HEADER, NULL, "global options:" }, +{ 'h', "help", "this help" }, +{ 'd', "debug", "debug mode" }, +{ 'v', "version", "print version of this program" }, +{ HEADER, NULL, "Available state options:" }, +{ FILEARG | 'f', "file", "configuration file (default " SYS_ASOUNDRC ")" }, +{ FILEARG | 'a', "config-dir", "boot / hotplug configuration directory (default " SYS_ASOUND_DIR ")" }, +{ 'l', "lock", "use file locking to serialize concurrent access" }, +{ 'L', "no-lock", "do not use file locking to serialize concurrent access" }, +{ FILEARG | 'K', "lock-dir", "lock path (default " SYS_LOCKPATH ")" }, +{ FILEARG | 'O', "lock-state-file", "state lock file path (default " SYS_LOCKFILE ")" }, +{ 'F', "force", "try to restore the matching controls as much as possible" }, +{ 0, NULL, " (default mode)" }, +{ 'g', "ignore", "ignore 'No soundcards found' error" }, +{ 'P', "pedantic", "do not restore mismatching controls (old default)" }, +{ 'I', "no-init-fallback", "" }, +{ 0, NULL, "don't initialize even if restore fails" }, +{ FILEARG | 'r', "runstate", "save restore and init state to this file (only errors)" }, +{ 0, NULL, " default settings is 'no file set'" }, +{ 'R', "remove", "remove runstate file at first, otherwise append errors" }, +{ INTARG | 'p', "period", "store period in seconds for the daemon command" }, +{ FILEARG | 'e', "pid-file", "pathname for the process id (daemon mode)" }, +{ HEADER, NULL, "Available init options:" }, +{ ENVARG | 'E', "env", "set environment variable for init phase (NAME=VALUE)" }, +{ FILEARG | 'i', "initfile", "main configuation file for init phase" }, +{ 0, NULL, " (default " DATADIR "/init/00main)" }, +{ 'b', "background", "run daemon in background" }, +{ 's', "syslog", "use syslog for messages" }, +{ INTARG | 'n', "nice", "set the process priority (see 'man nice')" }, +{ 'c', "sched-idle", "set the process scheduling policy to idle (SCHED_IDLE)" }, +#ifdef HAVE_ALSA_USE_CASE_H +{ 'D', "ucm-defaults", "execute also the UCM 'defaults' section" }, +{ 'U', "no-ucm", "don't init with UCM" }, +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION +{ 'X', "ucm-nodev", "show UCM no device errors" }, +#endif +#endif +{ HEADER, NULL, "Available commands:" }, +{ CARDCMD, "store", "save current driver setup for one or each soundcards" }, +{ EMPCMD, NULL, " to configuration file" }, +{ CARDCMD, "restore", "load current driver setup for one or each soundcards" }, +{ EMPCMD, NULL, " from configuration file" }, +{ CARDCMD, "nrestore", "like restore, but notify the daemon to rescan soundcards" }, +{ CARDCMD, "init", "initialize driver to a default state" }, +{ CARDCMD, "daemon", "store state periodically for one or each soundcards" }, +{ CARDCMD, "rdaemon", "like daemon but do the state restore at first" }, +{ KILLCMD, "kill", "notify daemon to quit, rescan or save_and_quit" }, +{ CARDCMD, "monitor", "monitor control events" }, +{ CARDCMD, "info", "general information" }, +{ CARDCMD, "clean", "clean application controls" }, +{ EMPCMD, "dump-state", "dump the state (for all cards)" }, +{ EMPCMD, "dump-cfg", "dump the configuration (expanded, for all cards)" }, +{ 0, NULL, NULL } +}; + +static void help(void) +{ + struct arg *n = args, *a; + char *larg, sa[4], buf[32]; + int sarg; + + sa[0] = '-'; + sa[2] = ','; + sa[3] = '\0'; + while (n->comment) { + a = n; + n++; + sarg = a->sarg; + if (sarg & (HEADER|TITLE)) { + printf("%s%s\n", (sarg & HEADER) != 0 ? "\n" : "", + a->comment); + continue; + } + buf[0] = '\0'; + larg = a->larg; + if (sarg & (EMPCMD|CARDCMD|KILLCMD)) { + if (sarg & CARDCMD) + strcat(buf, ""); + else if (sarg & KILLCMD) + strcat(buf, ""); + printf(" %-10s %-6s %s\n", larg ? larg : "", + buf, a->comment); + continue; + } + sa[1] = a->sarg; + sprintf(buf, "%s%s%s", sa[1] ? sa : "", + larg ? "--" : "", larg ? larg : ""); + if (sarg & ENVARG) + strcat(buf, " #=#"); + else if (sarg & (FILEARG|INTARG)) + strcat(buf, " #"); + printf(" %-15s %s\n", buf, a->comment); + } +} + +static int dump_config_tree(snd_config_t *top) +{ + snd_output_t *out; + int err; + + err = snd_output_stdio_attach(&out, stdout, 0); + if (err < 0) + return err; + err = snd_config_save(top, out); + snd_output_close(out); + return err; +} + +static int dump_state(const char *file) +{ + snd_config_t *top; + int err; + + err = load_configuration(file, &top, NULL); + if (err < 0) + return err; + err = dump_config_tree(top); + snd_config_delete(top); + return err; +} + +static int dump_configuration(void) +{ + snd_config_t *top, *cfg2; + int err; + + err = snd_config_update_ref(&top); + if (err < 0) + return err; + /* expand cards.* tree */ + err = snd_config_search_definition(top, "cards", "_dummy_", &cfg2); + if (err >= 0) + snd_config_delete(cfg2); + err = dump_config_tree(top); + snd_config_unref(top); + return err; +} + +#define NO_NICE (-100000) + +static void do_nice(int use_nice, int sched_idle) +{ + struct sched_param sched_param; + + if (use_nice != NO_NICE && nice(use_nice) < 0) + error("nice(%i): %s", use_nice, strerror(errno)); + if (sched_idle) { + if (sched_getparam(0, &sched_param) >= 0) { + sched_param.sched_priority = 0; + if (sched_setscheduler(0, SCHED_IDLE, &sched_param) < 0) + error("sched_setparam failed: %s", strerror(errno)); + } else { + error("sched_getparam failed: %s", strerror(errno)); + } + } +} + +int main(int argc, char *argv[]) +{ + static const char *const devfiles[] = { + "/dev/snd/controlC", + "/dev/snd/pcmC", + "/dev/snd/midiC", + "/dev/snd/hwC", + NULL + }; + char *cfgdir = SYS_ASOUND_DIR; + char *cfgfile = SYS_ASOUNDRC; + char *initfile = DATADIR "/init/00main"; + char *pidfile = SYS_PIDFILE; + char *cardname, ncardname[21]; + char *cmd; + char *const *extra_args; + const char *const *tmp; + int removestate = 0; + int init_fallback = 1; /* new default behavior */ + int period = 5*60; + int background = 0; + int daemoncmd = 0; + int use_nice = NO_NICE; + int sched_idle = 0; + int initflags = 0; + struct arg *a; + struct option *o; + int i, j, k, res; + struct option *long_option; + char *short_option; + +#if SND_LIB_VER(1, 2, 5) >= SND_LIB_VERSION + initflags |= FLAG_UCM_NODEV; +#endif + long_option = calloc(ARRAY_SIZE(args), sizeof(struct option)); + if (long_option == NULL) + exit(EXIT_FAILURE); + short_option = malloc(128); + if (short_option == NULL) { + free(long_option); + exit(EXIT_FAILURE); + } + for (i = j = k = 0; i < (int)ARRAY_SIZE(args); i++) { + a = &args[i]; + if ((a->sarg & 0xff) == 0) + continue; + o = &long_option[j]; + o->name = a->larg; + o->has_arg = (a->sarg & (ENVARG|FILEARG|INTARG)) != 0; + o->flag = NULL; + o->val = a->sarg & 0xff; + j++; + short_option[k++] = o->val; + if (o->has_arg) + short_option[k++] = ':'; + } + short_option[k] = '\0'; + command = argv[0]; + while (1) { + int c; + + if ((c = getopt_long(argc, argv, short_option, long_option, + NULL)) < 0) + break; + switch (c) { + case 'h': + help(); + res = EXIT_SUCCESS; + goto out; + case 'f': + cfgfile = optarg; + break; + case 'a': + cfgdir = optarg; + break; + case 'l': + do_lock = 1; + break; + case 'L': + do_lock = -1; + break; + case 'K': + lockpath = optarg; + break; + case 'O': + lockfile = optarg; + break; + case 'F': + force_restore = 1; + break; + case 'g': + ignore_nocards = 1; + break; + case 'E': + if (putenv(optarg)) { + fprintf(stderr, "environment string '%s' is wrong\n", optarg); + res = EXIT_FAILURE; + goto out; + } + break; + case 'i': + initfile = optarg; + break; + case 'I': + init_fallback = 0; + break; + case 'D': + initflags |= FLAG_UCM_DEFAULTS; + break; + case 'U': + initflags |= FLAG_UCM_DISABLED; + break; + case 'X': + initflags |= FLAG_UCM_NODEV; + break; + case 'r': + statefile = optarg; + break; + case 'R': + removestate = 1; + break; + case 'P': + force_restore = 0; + break; + case 'p': + period = atoi(optarg); + if (period < 10) + period = 5*60; + else if (period > 24*60*60) + period = 24*60*60; + break; + case 'e': + pidfile = optarg; + break; + case 'b': + background = 1; + break; + case 's': + use_syslog = 1; + break; + case 'n': + use_nice = atoi(optarg); + if (use_nice < -20) + use_nice = -20; + else if (use_nice > 19) + use_nice = 19; + break; + case 'c': + sched_idle = 1; + break; + case 'd': + debugflag = 1; + break; + case 'v': + printf("alsactl version " SND_UTIL_VERSION_STR "\n"); + res = EXIT_SUCCESS; + goto out; + case '?': // error msg already printed + help(); + res = EXIT_FAILURE; + goto out; + default: // should never happen + fprintf(stderr, + "Invalid option '%c' (%d) not handled??\n", c, c); + } + } + free(short_option); + short_option = NULL; + free(long_option); + long_option = NULL; + if (argc - optind <= 0) { + fprintf(stderr, "alsactl: Specify command...\n"); + res = 0; + goto out; + } + + cardname = argc - optind > 1 ? argv[optind + 1] : NULL; + for (tmp = devfiles; cardname != NULL && *tmp != NULL; tmp++) { + int len = strlen(*tmp); + if (!strncmp(cardname, *tmp, len)) { + long l = strtol(cardname + len, NULL, 0); + sprintf(ncardname, "%li", l); + cardname = ncardname; + break; + } + } + + extra_args = argc - optind > 2 ? argv + optind + 2 : NULL; + + /* the global system file should be always locked */ + if (strcmp(cfgfile, SYS_ASOUNDRC) == 0 && do_lock >= 0) + do_lock = 1; + + /* when running in background, use syslog for reports */ + if (background) { + use_syslog = 1; + if (daemon(0, 0)) { + syslog(LOG_INFO, "alsactl " SND_UTIL_VERSION_STR " daemon cannot be started: %s", strerror(errno)); + res = EXIT_FAILURE; + goto out; + } + } + + cmd = argv[optind]; + daemoncmd = strcmp(cmd, "daemon") == 0 || strcmp(cmd, "rdaemon") == 0; + + if (use_syslog) { + openlog("alsactl", LOG_CONS|LOG_PID, LOG_DAEMON); + if (daemoncmd) + syslog(LOG_INFO, "alsactl " SND_UTIL_VERSION_STR " daemon started"); + } + + snd_lib_error_set_handler(error_handler); + + if (!strcmp(cmd, "init")) { + res = init(cfgdir, initfile, initflags | FLAG_UCM_FBOOT | FLAG_UCM_BOOT, cardname); + snd_config_update_free_global(); + } else if (!strcmp(cmd, "store")) { + res = save_state(cfgfile, cardname); + } else if (!strcmp(cmd, "restore") || + !strcmp(cmd, "rdaemon") || + !strcmp(cmd, "nrestore")) { + if (removestate) + remove(statefile); + res = load_state(cfgdir, cfgfile, initfile, initflags, cardname, init_fallback); + if (!strcmp(cmd, "rdaemon")) { + do_nice(use_nice, sched_idle); + res = state_daemon(cfgfile, cardname, period, pidfile); + } + if (!strcmp(cmd, "nrestore")) + res = state_daemon_kill(pidfile, "rescan"); + } else if (!strcmp(cmd, "daemon")) { + do_nice(use_nice, sched_idle); + res = state_daemon(cfgfile, cardname, period, pidfile); + } else if (!strcmp(cmd, "kill")) { + res = state_daemon_kill(pidfile, cardname); + } else if (!strcmp(cmd, "monitor")) { + res = monitor(cardname); + } else if (!strcmp(cmd, "info")) { + res = general_info(cardname); + } else if (!strcmp(cmd, "clean")) { + res = clean(cardname, extra_args); + } else if (!strcmp(cmd, "dump-state")) { + res = dump_state(cfgfile); + } else if (!strcmp(cmd, "dump-cfg")) { + res = dump_configuration(); + } else { + fprintf(stderr, "alsactl: Unknown command '%s'...\n", cmd); + res = -ENODEV; + } + + snd_config_update_free_global(); + if (use_syslog) { + if (daemoncmd) + syslog(LOG_INFO, "alsactl daemon stopped"); + closelog(); + } + return res < 0 ? -res : 0; + +out: + free(short_option); + free(long_option); + return res; +} diff --git a/alsactl/alsactl.h b/alsactl/alsactl.h new file mode 100644 index 0000000..6ae45e3 --- /dev/null +++ b/alsactl/alsactl.h @@ -0,0 +1,92 @@ +#include +#include + +#define LOCK_TIMEOUT 10 + +extern int debugflag; +extern int force_restore; +extern int ignore_nocards; +extern int do_lock; +extern int use_syslog; +extern char *command; +extern char *statefile; +extern char *lockpath; +extern char *lockfile; + +struct snd_card_iterator { + int card; + char name[16]; + bool single; + bool first; +}; + +void info_(const char *fcn, long line, const char *fmt, ...); +void error_(const char *fcn, long line, const char *fmt, ...); +void cerror_(const char *fcn, long line, int cond, const char *fmt, ...); +void dbg_(const char *fcn, long line, const char *fmt, ...); +void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...); + +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95) +#define info(...) do { info_(__func__, __LINE__, __VA_ARGS__); } while (0) +#define error(...) do { error_(__func__, __LINE__, __VA_ARGS__); } while (0) +#define cerror(cond, ...) do { cerror_(__func__, __LINE__, (cond) != 0, __VA_ARGS__); } while (0) +#define dbg(...) do { dbg_(__func__, __LINE__, __VA_ARGS__); } while (0) +#else +#define info(args...) do { info_(__func__, __LINE__, ##args); } while (0) +#define error(args...) do { error_(__func__, __LINE__, ##args); } while (0) +#define cerror(cond, ...) do { error_(__func__, __LINE__, (cond) != 0, ##args); } while (0) +#define dbg(args...) do { dbg_(__func__, __LINE__, ##args); } while (0) +#endif + +#define FLAG_UCM_DISABLED (1<<0) +#define FLAG_UCM_FBOOT (1<<1) +#define FLAG_UCM_BOOT (1<<2) +#define FLAG_UCM_DEFAULTS (1<<3) +#define FLAG_UCM_NODEV (1<<4) + +void snd_card_iterator_init(struct snd_card_iterator *iter, int cardno); +int snd_card_iterator_sinit(struct snd_card_iterator *iter, const char *cardname); +const char *snd_card_iterator_next(struct snd_card_iterator *iter); +int snd_card_iterator_error(struct snd_card_iterator *iter); + +int load_configuration(const char *file, snd_config_t **top, int *open_failed); +int init(const char *cfgdir, const char *file, int flags, const char *cardname); +int init_ucm(int flags, int cardno); +int state_lock(const char *file, int timeout); +int state_unlock(int lock_fd, const char *file); +int card_lock(int card_number, int timeout); +int card_unlock(int lock_fd, int card_number); +int save_state(const char *file, const char *cardname); +int load_state(const char *cfgdir, const char *file, + const char *initfile, int initflags, + const char *cardname, int do_init); +int power(const char *argv[], int argc); +int monitor(const char *name); +int general_info(const char *name); +int state_daemon(const char *file, const char *cardname, int period, + const char *pidfile); +int state_daemon_kill(const char *pidfile, const char *cmd); +int clean(const char *cardname, char *const *extra_args); +int snd_card_clean_cfgdir(const char *cfgdir, int cardno); + +/* utils */ + +int file_map(const char *filename, char **buf, size_t *bufsize); +void file_unmap(void *buf, size_t bufsize); +size_t line_width(const char *buf, size_t bufsize, size_t pos); +void initfailed(int cardnumber, const char *reason, int exitcode); + +static inline int hextodigit(int c) +{ + if (c >= '0' && c <= '9') + c -= '0'; + else if (c >= 'a' && c <= 'f') + c = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + c = c - 'A' + 10; + else + return -1; + return c; +} + +#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a)[0]) diff --git a/alsactl/alsactl_init.xml b/alsactl/alsactl_init.xml new file mode 100644 index 0000000..bce26f5 --- /dev/null +++ b/alsactl/alsactl_init.xml @@ -0,0 +1,633 @@ + + + +
+
+ alsactl init + + + alsactl init + July 2008 + alsactl + + + + alsactl_init + 7 + + + + + alsactl init + alsa control management - initialization + + + DESCRIPTION + "alsactl init" provides soundcard specific initialization. + + + CONFIGURATION + All "alsactl init" configuration files are placed in + /usr/share/alsa/init/ directory. The top level + configuration file is /usr/share/alsa/init/00main. + The default top-level file can be also specified using -i or + --initfile parameter for the alsactl tool. + Every file consists of a set of lines of text. All empty lines or + lines beginning with '#' will be ignored. + + Rules files + The "alsactl init" rules are read from the files located + in the /usr/share/alsa/init/*. The top + level configuration file is /usr/share/alsa/init/00main. + Every line in the rules file contains at least one key value pair. + There are two kind of keys, match and assignment keys. If all match + keys are matching against its value, the rule gets applied and the + assign keys get the specified value assigned. + + A rule may consists of a list of one or more key value pairs + separated by a comma. Each key has a distinct operation, depending + on the used operator. Valid operators are: + + + + + Compare for equality. + + + + + + + Compare for non-equality. + + + + + + + Assign a value to a key. Keys that represent a list, + are reset and only this single value is assigned. + + + + + + + Add the value to a key that holds a list + of entries. + + + + + + + Assign a value to a key finally; disallow any + later changes, which may be used to prevent changes by + any later rules. + + + + + The following key names can be used to match against device + properties: + + + + + Match the card index of the ALSA driver. + + + + + + + Set or test universal control attribute. Possible + attributes: + + + + + Numeric control identification. + + + + , + + Control interface name (CARD, HWEDEP, MIXER, PCM, RAWMIDI, TIMER, SEQUENCER) + + + + , + + Subdevice number. + + + + + + Control name + + + + + + Control index + + + + + + Control type (BOOLEAN, INTEGER, INTEGER64, ENUMERATED, BYTES, IEC958) + + + + , + + Attributes (stored in a string - use match characters * and ?): + + + + + control is readable + + + + + + control is writable + + + + + + control is volatile + + + + + + control is inactive + + + + + + control is locked + + + + + + control is TLV readable + + + + + + control is TLV writable + + + + + + control is TLV commandable + + + + + + process is owner of this control + + + + + + control created in user space + + + + + + + + + Control owner process PID number + + + + + + Control count of values + + + + + + Value range - minimum value + + + + + + Value range - maximum value + + + + + + Value range - step value + + + + + + Value range - minimum dB value + + + + + + Value range - maximum dB value + + + + + + Enumerated value - number of text items + + + + + + Enumerated value - list of text names stored between '|' character + + + + + + Value of control stored to a string delimited by + comma (,). + + + + + + Search for a control. Value "1" is returned + if a control was found. The CTL{name} key might + contain match characters * and ?. An control index + might be specified as first argument starting from + zero (e.g. CTL{do_search 2}="1"). + + + + + + Search for a controls and return total count + of matched ones. The CTL{name} key might contain match + characters * and ?. + + + + + + + + + + The relative path to sysfs subsystem specifying + the root directory of a soundcard device. Usually, + it should be set to "/class/sound/card$cardinfo{card}/device". + + + + + + + + Match sysfs attribute values of the soundcard device. + The relative path to sysfs tree must be defined by + CONFIG{sysfs_device} key. Trailing whitespace in the attribute + values is ignored, if the specified match value does + not contain trailing whitespace itself. Depending on + the type of operator, this key is also used to set + the value of a sysfs attribute. + + + + + + + + Match against the value of an environment variable. Up + to five keys can be specified per rule. + Depending on the type of operator, this key is also used + to export a variable to the environment. + + + + + + + Execute external program. The key is true, if + the program returns without exit code zero. The whole event + environment is available to the executed program. The + program's output printed to stdout is available for + the RESULT key. + Several buildin commands are available: + + + + + Search for a control. The CTL{name} key might + contain match characters * and ?. An control index + might be specified as first argument starting from + zero (e.g. PROGRAM="__ctl_search 2"). + + + + + + Search for a controls and return total count + of matched ones. The CTL{name} key might contain match + characters * and ?. + + + + + + + + + + Match the returned string of the last PROGRAM call. + This key can be used in the same or in any later rule + after a PROGRAM call. + + + + + + Most of the fields support a shell style pattern matching. + The following pattern characters are supported: + + + + + Matches zero, or any number of characters. + + + + + + Matches any single character. + + + + + + Matches any single character specified within + the brackets. For example, the pattern string 'tty[SR]' + would match either 'ttyS' or 'ttyR'. Ranges are also + supported within this match with the '-' character. + For example, to match on the range of all digits, + the pattern [0-9] would be used. If the first character + following the '[' is a '!', any characters + not enclosed are matched. + + + + + The following keys can get values assigned: + + + , , + , , + , , + + + Select universal control element. + + + + + + Value is set (written) also to soundcard's control + device and RESULT key is set to errno code. The result of + set operation is always true (it means continue with + next key on line). + + + + + + Value is set (written) also to soundcard's control + device (all control values are set to specified value) and + RESULT key is set to errno code. The result of + set operation is always true (it means continue with + next key on line). + + + + + + Value is set (written) also to soundcard's control + device (all control values are set to specified value). + The result of set operation is true when operation + succeed (it means continue with next key on line). + + + + + + + Export a variable to the environment. Depending on the type of operator, + this key is also to match against an environment variable. + + + + + + + Set RESULT variable. Note that PROGRAM also sets + this variable, but setting this variable manually + might be useful to change code execution order (included + files). + + + + + + + Named label where a GOTO can jump to. + + + + + + + Jumps to the next LABEL with a matching name. + The goto cannot jump backward. + + + + + + + Include the specified filename or files in specified directory. + + + When a directory is specified, only the files with the + extension ".conf" are read. + Also they are read in the alphabetical order. + Thus it's highly recommended to use some number prefix + (e.g. "01-something.conf") to assure the order of execucions. + + + + + + + + Check if specified file or directory exists + + + + + + + The relative path to sysfs subsystem specifying + the root directory of a soundcard device. Usually, + it should be set to "/class/sound/card$cardinfo{card}/device". + + + + + + + + PRINT value to stdout. + + + + + + + PRINT value to stderr. + + + + + + + Exit immediately and set program exit code to value + (should be integer). If value is "return" string, + parser leaves current included file and returns to parent + configuration file. + + + + + + The , , + , + , , + , + fields support simple printf-like string substitutions. + It allows the use of the complete environment set by earlier matching + rules. For all other fields, substitutions are applied while the individual rule is + being processed. The available substitutions are: + + + , + + See CARDINFO{} for more details. + + + + + , + + See CTL{} for more details. + + + + + , + + The value of a sysfs attribute found at the device, where + all keys of the rule have matched. + If the attribute is a symlink, the last element of the symlink target is + returned as the value. + + + + + , + + The value of an environment variable. + + + + + , + + The string returned by the external program requested with PROGRAM. + A single part of the string, separated by a space character may be selected + by specifying the part number as an attribute: . + If the number is followed by the '+' char this part plus all remaining parts + of the result string are substituted: + + + + + , + + Root directory where sysfs file-system is mounted. + Ususally, this value is just "/sys". + + + + + , + + The value of a configuration variable. See CONFIG{} for more details. + + + + + + + The '%' character itself. + + + + + + + The '$' character itself. + + + + The count of characters to be substituted may be limited + by specifying the format length value. For example, '%3s{file}' + will only insert the first three characters of the sysfs + attribute + + + + AUTHOR + Written by Jaroslav Kysela perex@perex.cz + Some portions are written by Greg Kroah-Hartman greg@kroah.com and + Kay Sievers kay.sievers@vrfy.org. + + + + SEE ALSO + + alsactl1 + + + +
+
diff --git a/alsactl/clean.c b/alsactl/clean.c new file mode 100644 index 0000000..1eb82c0 --- /dev/null +++ b/alsactl/clean.c @@ -0,0 +1,196 @@ +/* + * Advanced Linux Sound Architecture Control Program + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include "alsactl.h" + +static int clean_one_control(snd_ctl_t *handle, snd_ctl_elem_id_t *elem_id, + snd_ctl_elem_id_t **filter) +{ + snd_ctl_elem_info_t *info; + char *s; + int err; + + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_info_set_id(info, elem_id); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + s = snd_ctl_ascii_elem_id_get(elem_id); + error("Cannot read control info '%s': %s", s, snd_strerror(err)); + free(s); + return err; + } + + if (!snd_ctl_elem_info_is_user(info)) + return 0; + + s = snd_ctl_ascii_elem_id_get(elem_id); + dbg("Application control \"%s\" found.", s); + if (filter) { + for (; *filter; filter++) { + if (snd_ctl_elem_id_compare_set(elem_id, *filter) == 0) + break; + } + if (*filter == NULL) { + free(s); + return 0; + } + } + + err = snd_ctl_elem_remove(handle, elem_id); + if (err < 0) { + error("Cannot remove control '%s': %s", s, snd_strerror(err)); + free(s); + return err; + } + dbg("Application control \"%s\" removed.", s); + free(s); + return 0; +} + +static void filter_controls_free(snd_ctl_elem_id_t **_filter) +{ + snd_ctl_elem_id_t **filter; + + for (filter = _filter; filter; filter++) + free(*filter); + free(_filter); +} + +static int filter_controls_parse(char *const *controls, snd_ctl_elem_id_t ***_filter) +{ + snd_ctl_elem_id_t **filter = NULL; + char *const *c; + char *s; + unsigned int count, idx; + int err; + + if (!controls) + goto fin; + for (count = 0, c = controls; *c; c++, count++); + if (count == 0) + goto fin; + filter = calloc(count + 1, sizeof(snd_ctl_elem_id_t *)); + if (filter == NULL) { +nomem: + error("No enough memory..."); + return -ENOMEM; + } + filter[count] = NULL; + for (idx = 0; idx < count; idx++) { + err = snd_ctl_elem_id_malloc(&filter[idx]); + if (err < 0) { + filter_controls_free(filter); + goto nomem; + } + err = snd_ctl_ascii_elem_id_parse(filter[idx], controls[idx]); + if (err < 0) { + error("Cannot parse id '%s': %s", controls[idx], snd_strerror(err)); + filter_controls_free(filter); + return err; + } + s = snd_ctl_ascii_elem_id_get(filter[idx]); + dbg("Add to filter: \"%s\"", s); + free(s); + } +fin: + *_filter = filter; + return 0; +} + +static int clean_controls(int cardno, char *const *controls) +{ + snd_ctl_t *handle; + snd_ctl_elem_list_t *list; + snd_ctl_elem_id_t *elem_id; + snd_ctl_elem_id_t **filter; + char name[32]; + unsigned int idx, count; + int err; + + snd_ctl_elem_id_alloca(&elem_id); + snd_ctl_elem_list_alloca(&list); + + err = filter_controls_parse(controls, &filter); + if (err < 0) + return err; + + sprintf(name, "hw:%d", cardno); + err = snd_ctl_open(&handle, name, 0); + if (err < 0) { + error("snd_ctl_open error: %s", snd_strerror(err)); + filter_controls_free(filter); + return err; + } + dbg("Control device '%s' opened.", name); + err = snd_ctl_elem_list(handle, list); + if (err < 0) { + error("Cannot determine controls: %s", snd_strerror(err)); + goto fin_err; + } + count = snd_ctl_elem_list_get_count(list); + if (count == 0) + goto fin_ok; + snd_ctl_elem_list_set_offset(list, 0); + if ((err = snd_ctl_elem_list_alloc_space(list, count)) < 0) { + error("No enough memory..."); + goto fin_err; + } + if ((err = snd_ctl_elem_list(handle, list)) < 0) { + error("Cannot determine controls (2): %s", snd_strerror(err)); + goto fin_err; + } + for (idx = 0; idx < count; idx++) { + snd_ctl_elem_list_get_id(list, idx, elem_id); + err = clean_one_control(handle, elem_id, filter); + if (err < 0) + goto fin_err; + } +fin_ok: + filter_controls_free(filter); + snd_ctl_close(handle); + return 0; +fin_err: + filter_controls_free(filter); + snd_ctl_close(handle); + return err; +} + +int clean(const char *cardname, char *const *extra_args) +{ + struct snd_card_iterator iter; + int err; + + err = snd_card_iterator_sinit(&iter, cardname); + if (err < 0) + return err; + while (snd_card_iterator_next(&iter)) { + if ((err = clean_controls(iter.card, extra_args))) + return err; + } + return snd_card_iterator_error(&iter); +} diff --git a/alsactl/daemon.c b/alsactl/daemon.c new file mode 100644 index 0000000..4b9d392 --- /dev/null +++ b/alsactl/daemon.c @@ -0,0 +1,461 @@ +/* + * Advanced Linux Sound Architecture Control Program + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "alsactl.h" + +struct id_list { + snd_ctl_elem_id_t **list; + int size; +}; + +struct card { + int index; + int pfds; + snd_ctl_t *handle; + struct id_list whitelist; + struct id_list blacklist; +}; + +static int quit = 0; +static int rescan = 0; +static int save_now = 0; + +static void signal_handler_quit(int sig) +{ + quit = 1; + signal(sig, signal_handler_quit); +} + +static void signal_handler_save_and_quit(int sig) +{ + quit = save_now = 1; + signal(sig, signal_handler_quit); +} + +static void signal_handler_rescan(int sig) +{ + rescan = 1; + signal(sig, signal_handler_rescan); +} + +static void free_list(struct id_list *list) +{ + int i; + + for (i = 0; i < list->size; i++) + free(list->list[i]); + free(list->list); +} + +static void card_free(struct card **card) +{ + struct card *c = *card; + + if (c == NULL) + return; + free_list(&c->blacklist); + free_list(&c->whitelist); + if (c->handle) + snd_ctl_close(c->handle); + free(c); + *card = NULL; +} + +static void add_card(struct card ***cards, int *count, const char *cardname) +{ + struct card *card, **cc; + int i, index, findex; + char device[16]; + + index = snd_card_get_index(cardname); + if (index < 0) + return; + for (i = 0, findex = -1; i < *count; i++) { + if ((*cards)[i] == NULL) { + findex = i; + } else { + if ((*cards)[i]->index == index) + return; + } + } + card = calloc(1, sizeof(*card)); + if (card == NULL) + return; + card->index = index; + sprintf(device, "hw:%i", index); + if (snd_ctl_open(&card->handle, device, SND_CTL_READONLY|SND_CTL_NONBLOCK) < 0) { + card_free(&card); + return; + } + card->pfds = snd_ctl_poll_descriptors_count(card->handle); + if (card->pfds < 0) { + card_free(&card); + return; + } + if (snd_ctl_subscribe_events(card->handle, 1) < 0) { + card_free(&card); + return; + } + if (findex >= 0) { + (*cards)[findex] = card; + } else { + cc = realloc(*cards, sizeof(void *) * (*count + 1)); + if (cc == NULL) { + card_free(&card); + return; + } + cc[*count] = card; + *count = *count + 1; + *cards = cc; + } +} + +static void add_cards(struct card ***cards, int *count) +{ + int card = -1; + char cardname[16]; + + while (1) { + if (snd_card_next(&card) < 0) + break; + if (card < 0) + break; + if (card >= 0) { + sprintf(cardname, "%i", card); + add_card(cards, count, cardname); + } + } +} + +static int compare_ids(snd_ctl_elem_id_t *id1, snd_ctl_elem_id_t *id2) +{ + if (id1 == NULL || id2 == NULL) + return 0; + return snd_ctl_elem_id_get_interface(id1) == snd_ctl_elem_id_get_interface(id2) && + snd_ctl_elem_id_get_index(id1) == snd_ctl_elem_id_get_index(id2) && + strcmp(snd_ctl_elem_id_get_name(id1), snd_ctl_elem_id_get_name(id2)) == 0 && + snd_ctl_elem_id_get_device(id1) == snd_ctl_elem_id_get_device(id2) && + snd_ctl_elem_id_get_subdevice(id1) == snd_ctl_elem_id_get_subdevice(id2); +} + +static int in_list(struct id_list *list, snd_ctl_elem_id_t *id) +{ + int i; + snd_ctl_elem_id_t *id1; + + for (i = 0; i < list->size; i++) { + id1 = list->list[i]; + if (id1 == NULL) + continue; + if (compare_ids(id, id1)) + return 1; + } + return 0; +} + +static void remove_from_list(struct id_list *list, snd_ctl_elem_id_t *id) +{ + int i; + + for (i = 0; i < list->size; i++) { + if (compare_ids(id, list->list[i])) { + free(list->list[i]); + list->list[i] = NULL; + } + } +} + +static void add_to_list(struct id_list *list, snd_ctl_elem_id_t *id) +{ + snd_ctl_elem_id_t *id1; + snd_ctl_elem_id_t **n; + int i; + + if (snd_ctl_elem_id_malloc(&id1)) + return; + snd_ctl_elem_id_copy(id1, id); + for (i = 0; i < list->size; i++) { + if (list->list[i] == NULL) { + list->list[i] = id1; + return; + } + } + n = realloc(list->list, sizeof(void *) * (list->size + 1)); + if (n == NULL) + return; + n[list->size] = id1; + list->size++; + list->list = n; +} + +static int check_lists(struct card *card, snd_ctl_elem_id_t *id) +{ + snd_ctl_elem_info_t *info; + snd_ctl_elem_info_alloca(&info); + + if (in_list(&card->blacklist, id)) + return 0; + if (in_list(&card->whitelist, id)) + return 1; + snd_ctl_elem_info_set_id(info, id); + if (snd_ctl_elem_info(card->handle, info) < 0) + return 0; + if (snd_ctl_elem_info_is_writable(info) || + snd_ctl_elem_info_is_tlv_writable(info)) { + add_to_list(&card->whitelist, id); + return 1; + } else { + add_to_list(&card->blacklist, id); + return 0; + } +} + +static int card_events(struct card *card) +{ + int res = 0; + snd_ctl_event_t *ev; + snd_ctl_event_type_t type; + unsigned int mask; + snd_ctl_elem_id_t *id; + snd_ctl_event_alloca(&ev); + snd_ctl_elem_id_alloca(&id); + + while (snd_ctl_read(card->handle, ev) == 1) { + type = snd_ctl_event_get_type(ev); + if (type != SND_CTL_EVENT_ELEM) + continue; + mask = snd_ctl_event_elem_get_mask(ev); + snd_ctl_event_elem_get_id(ev, id); + if (mask == SND_CTL_EVENT_MASK_REMOVE) { + remove_from_list(&card->whitelist, id); + remove_from_list(&card->blacklist, id); + continue; + } + if (mask & SND_CTL_EVENT_MASK_INFO) { + remove_from_list(&card->whitelist, id); + remove_from_list(&card->blacklist, id); + } + if (mask & (SND_CTL_EVENT_MASK_VALUE| + SND_CTL_EVENT_MASK_ADD| + SND_CTL_EVENT_MASK_TLV)) { + if (check_lists(card, id)) + res = 1; + } + } + return res; +} + +static long read_pid_file(const char *pidfile) +{ + int fd, err; + char pid_txt[12]; + + fd = open(pidfile, O_RDONLY); + if (fd >= 0) { + err = read(fd, pid_txt, 11); + if (err != 11) + err = err < 0 ? -errno : -EIO; + close(fd); + pid_txt[11] = '\0'; + return err < 0 ? err : atol(pid_txt); + } else { + return -errno; + } +} + +static int write_pid_file(const char *pidfile) +{ + int fd, err; + char pid_txt[14]; + + sprintf(pid_txt, "%10li\n", (long)getpid()); + fd = open(pidfile, O_WRONLY|O_CREAT|O_EXCL, 0600); + if (fd >= 0) { + err = write(fd, pid_txt, 11); + if (err != 11) { + err = err < 0 ? -errno : -EIO; + unlink(pidfile); + } else { + err = 0; + } + close(fd); + } else { + err = -errno; + } + return err; +} + +int state_daemon_kill(const char *pidfile, const char *cmd) +{ + long pid; + int sig = SIGHUP; + + if (cmd == NULL) { + error("Specify kill command (quit, rescan or save_and_quit)"); + return -EINVAL; + } + if (strcmp(cmd, "rescan") == 0) + sig = SIGUSR1; + else if (strcmp(cmd, "save_and_quit") == 0) + sig = SIGUSR2; + else if (strcmp(cmd, "quit") == 0) + sig = SIGTERM; + if (sig == SIGHUP) { + error("Unknown kill command '%s'", cmd); + return -EINVAL; + } + pid = read_pid_file(pidfile); + if (pid > 0) { + if (kill(pid, sig) >= 0) + return 0; + return -errno; + } + return 0; +} + +static int check_another_instance(const char *pidfile) +{ + long pid; + + pid = read_pid_file(pidfile); + if (pid >= 0) { + /* invoke new card rescan */ + if (kill(pid, SIGUSR1) >= 0) { + usleep(1000); + pid = read_pid_file(pidfile); + if (pid >= 0) + return 1; + } + } + return 0; +} + +int state_daemon(const char *file, const char *cardname, int period, + const char *pidfile) +{ + int count = 0, pcount, psize = 0, i, j, k, changed = 0; + time_t last_write, now; + unsigned short revents; + struct card **cards = NULL; + struct pollfd *pfd = NULL, *pfdn; + + if (check_another_instance(pidfile)) + return 0; + rescan = 1; + signal(SIGABRT, signal_handler_quit); + signal(SIGTERM, signal_handler_quit); + signal(SIGINT, signal_handler_quit); + signal(SIGUSR1, signal_handler_rescan); + signal(SIGUSR2, signal_handler_save_and_quit); + write_pid_file(pidfile); + time(&last_write); + while (!quit || save_now) { + if (save_now) + goto save; + if (rescan) { + if (cardname) { + add_card(&cards, &count, cardname); + } else { + add_cards(&cards, &count); + } + snd_config_update_free_global(); + rescan = 0; + } + for (i = pcount = 0; i < count; i++) { + if (cards[i] == NULL) + continue; + pcount += cards[i]->pfds; + } + if (pcount > psize) { + pfdn = realloc(pfd, sizeof(struct pollfd) * pcount); + if (pfdn) { + psize = pcount; + pfd = pfdn; + } else { + error("No enough memory..."); + goto out; + } + } + for (i = j = 0; i < count; i++) { + if (cards[i] == NULL) + continue; + k = snd_ctl_poll_descriptors(cards[i]->handle, pfd + j, pcount - j); + if (k != cards[i]->pfds) { + error("poll prepare failed: %i", k); + goto out; + } + j += k; + } + i = poll(pfd, j, (period / 2) * 1000); + if (i < 0 && errno == EINTR) + continue; + if (i < 0) { + error("poll failed: %s", strerror(errno)); + break; + } + time(&now); + for (i = j = 0; i < count; i++) { + if (cards[i] == NULL) + continue; + k = snd_ctl_poll_descriptors_revents(cards[i]->handle, + pfd + j, cards[i]->pfds, &revents); + if (k < 0) { + error("poll post failed: %i\n", k); + goto out; + } + j += cards[i]->pfds; + if (revents & (POLLERR|POLLNVAL)) { + card_free(&cards[i]); + } else if (revents & POLLIN) { + if (card_events(cards[i])) { + /* delay the write */ + if (!changed) + last_write = now; + changed = 1; + } + } + } + if ((now - last_write >= period && changed) || save_now) { +save: + changed = save_now = 0; + save_state(file, cardname); + } + } +out: + free(pfd); + remove(pidfile); + if (cards) { + for (i = 0; i < count; i++) + card_free(&cards[i]); + free(cards); + } + return 0; +} diff --git a/alsactl/info.c b/alsactl/info.c new file mode 100644 index 0000000..9acd701 --- /dev/null +++ b/alsactl/info.c @@ -0,0 +1,268 @@ +/* + * Advanced Linux Sound Architecture Control Program - General info + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "alsactl.h" + +static int pcm_device_list(snd_ctl_t *ctl, snd_pcm_stream_t stream, bool *first) +{ +#ifdef __ALSA_PCM_H + int err, dev, idx; + unsigned int count; + snd_pcm_info_t *pcminfo; + snd_pcm_info_alloca(&pcminfo); + bool streamfirst, subfirst; + + dev = -1; + streamfirst = true; + while (1) { + if ((err = snd_ctl_pcm_next_device(ctl, &dev)) < 0) { + if (err == ENOTTY) /* no kernel support */ + return 0; + error("snd_ctl_pcm_next_device: %s", snd_strerror(err)); + return err; + } + if (dev < 0) + break; + snd_pcm_info_set_device(pcminfo, dev); + snd_pcm_info_set_subdevice(pcminfo, 0); + snd_pcm_info_set_stream(pcminfo, stream); + if ((err = snd_ctl_pcm_info(ctl, pcminfo)) < 0) { + if (err != -ENOENT) + return err; + continue; + } + if (*first) { + printf(" pcm:\n"); + *first = false; + } + if (streamfirst) { + printf(" - stream: %s\n devices:\n", snd_pcm_stream_name(stream)); + streamfirst = false; + } + printf(" - device: %d\n id: %s\n name: %s\n", + dev, + snd_pcm_info_get_id(pcminfo), + snd_pcm_info_get_name(pcminfo)); + count = snd_pcm_info_get_subdevices_count(pcminfo); + subfirst = true; + for (idx = 0; idx < (int)count; idx++) { + snd_pcm_info_set_subdevice(pcminfo, idx); + if ((err = snd_ctl_pcm_info(ctl, pcminfo)) < 0) { + error("control digital audio playback info (%s): %s", snd_ctl_name(ctl), snd_strerror(err)); + return err; + } + if (subfirst) { + printf(" subdevices:\n"); + subfirst = false; + } + printf(" - subdevice: %d\n name: %s\n", + idx, snd_pcm_info_get_subdevice_name(pcminfo)); + } + } +#endif + return 0; +} + +static const char *snd_rawmidi_stream_name(snd_rawmidi_stream_t stream) +{ + if (stream == SND_RAWMIDI_STREAM_INPUT) + return "INPUT"; + if (stream == SND_RAWMIDI_STREAM_OUTPUT) + return "OUTPUT"; + return "???"; +} + +static int rawmidi_device_list(snd_ctl_t *ctl, snd_rawmidi_stream_t stream, bool *first) +{ +#ifdef __ALSA_RAWMIDI_H + int err, dev, idx; + unsigned int count; + snd_rawmidi_info_t *info; + snd_rawmidi_info_alloca(&info); + bool streamfirst, subfirst; + + dev = -1; + streamfirst = true; + while (1) { + if ((err = snd_ctl_rawmidi_next_device(ctl, &dev)) < 0) { + if (err == ENOTTY) /* no kernel support */ + return 0; + error("snd_ctl_rawmidi_next_device: %s", snd_strerror(err)); + return err; + } + if (dev < 0) + break; + snd_rawmidi_info_set_device(info, dev); + snd_rawmidi_info_set_subdevice(info, 0); + snd_rawmidi_info_set_stream(info, stream); + if ((err = snd_ctl_rawmidi_info(ctl, info)) < 0) { + if (err != -ENOENT) + return err; + continue; + } + if (*first) { + printf(" rawmidi:\n"); + *first = false; + } + if (streamfirst) { + printf(" - stream: %s\n devices:\n", snd_rawmidi_stream_name(stream)); + streamfirst = false; + } + printf(" - device: %d\n id: %s\n name: %s\n", + dev, + snd_rawmidi_info_get_id(info), + snd_rawmidi_info_get_name(info)); + count = snd_rawmidi_info_get_subdevices_count(info); + subfirst = true; + for (idx = 0; idx < (int)count; idx++) { + snd_rawmidi_info_set_subdevice(info, idx); + if ((err = snd_ctl_rawmidi_info(ctl, info)) < 0) { + error("control digital audio playback info (%s): %s", snd_ctl_name(ctl), snd_strerror(err)); + return err; + } + if (subfirst) { + printf(" subdevices:\n"); + subfirst = false; + } + printf(" - subdevice: %d\n name: %s\n", + idx, snd_rawmidi_info_get_subdevice_name(info)); + } + } +#endif + return 0; +} + +static int hwdep_device_list(snd_ctl_t *ctl) +{ +#ifdef __ALSA_HWDEP_H + int err, dev; + snd_hwdep_info_t *info; + snd_hwdep_info_alloca(&info); + bool first; + + dev = -1; + first = true; + while (1) { + if ((err = snd_ctl_hwdep_next_device(ctl, &dev)) < 0) { + if (err == ENOTTY) /* no kernel support */ + return 0; + error("snd_ctl_pcm_next_device: %s", snd_strerror(err)); + return err; + } + if (dev < 0) + break; + snd_hwdep_info_set_device(info, dev); + if ((err = snd_ctl_hwdep_info(ctl, info)) < 0) { + if (err != -ENOENT) + return err; + continue; + } + if (first) { + printf(" hwdep:\n"); + first = false; + } + printf(" - device: %d\n id: %s\n name: %s\n iface: %d\n", + dev, + snd_hwdep_info_get_id(info), + snd_hwdep_info_get_name(info), + snd_hwdep_info_get_iface(info)); + } +#endif + return 0; +} + +static int card_info(snd_ctl_t *ctl) +{ + snd_ctl_card_info_t *info; + snd_ctl_elem_list_t *clist; + int err; + + snd_ctl_card_info_alloca(&info); + snd_ctl_elem_list_alloca(&clist); + + if ((err = snd_ctl_card_info(ctl, info)) < 0) { + error("Control device %s hw info error: %s", snd_ctl_name(ctl), snd_strerror(err)); + return err; + } + printf("#\n# Sound card\n#\n"); + printf("- card: %i\n id: %s\n name: %s\n longname: %s\n", + snd_ctl_card_info_get_card(info), + snd_ctl_card_info_get_id(info), + snd_ctl_card_info_get_name(info), + snd_ctl_card_info_get_longname(info)); + printf(" driver_name: %s\n", snd_ctl_card_info_get_driver(info)); + printf(" mixer_name: %s\n", snd_ctl_card_info_get_mixername(info)); + printf(" components: %s\n", snd_ctl_card_info_get_components(info)); + if ((err = snd_ctl_elem_list(ctl, clist)) < 0) { + error("snd_ctl_elem_list failure: %s", snd_strerror(err)); + } else { + printf(" controls_count: %i\n", snd_ctl_elem_list_get_count(clist)); + } + return err; +} + +int general_card_info(int cardno) +{ + snd_ctl_t *ctl; + char dev[16]; + bool first; + int err; + + snprintf(dev, sizeof(dev), "hw:%i", cardno); + if ((err = snd_ctl_open(&ctl, dev, 0)) < 0) { + error("Control device %s open error: %s", dev, snd_strerror(err)); + return err; + } + err = card_info(ctl); + + first = true; + if (err >= 0) + err = pcm_device_list(ctl, SND_PCM_STREAM_PLAYBACK, &first); + if (err >= 0) + err = pcm_device_list(ctl, SND_PCM_STREAM_CAPTURE, &first); + + first = true; + if (err >= 0) + err = rawmidi_device_list(ctl, SND_RAWMIDI_STREAM_INPUT, &first); + if (err >= 0) + err = rawmidi_device_list(ctl, SND_RAWMIDI_STREAM_OUTPUT, &first); + + if (err >= 0) + err = hwdep_device_list(ctl); + snd_ctl_close(ctl); + return err; +} + +int general_info(const char *cardname) +{ + struct snd_card_iterator iter; + int err; + + err = snd_card_iterator_sinit(&iter, cardname); + if (err < 0) + return err; + while (snd_card_iterator_next(&iter)) { + if ((err = general_card_info(iter.card))) + return err; + } + return snd_card_iterator_error(&iter); +} diff --git a/alsactl/init/00main b/alsactl/init/00main new file mode 100644 index 0000000..4f01b08 --- /dev/null +++ b/alsactl/init/00main @@ -0,0 +1,49 @@ +# This is the toplevel configuration for 'alsactl init'. +# See 'man alsactl_init' for syntax. + +# set root device directory in sysfs for soundcard for ATTR{} command +CONFIG{sysfs_device}="/class/sound/card$cardinfo{card}/device" +ACCESS!="$sysfsroot$config{sysfs_device}", \ + CONFIG{sysfs_device}="/class/sound/controlC$cardinfo{card}/device" + +# test for extra commands +ENV{CMD}=="help", INCLUDE="help", GOTO="00main_end" +ENV{CMD}=="info", INCLUDE="info", GOTO="00main_end" +ENV{CMD}=="default", INCLUDE="default", GOTO="00main_end" +ENV{CMD}=="test", INCLUDE="test", GOTO="00main_end" +ENV{CMD}=="*", ERROR="Unknown command '$env{CMD}'\n", GOTO="00main_end" + +# include files with real configuration +# +# steps are: +# 1) look for preinit subdirectory and parse all files in it +# 2) if RESULT=="skip", skip ALSA standard configuration files +# 3) do ALSA standard configuration +# 4) look for postinit subdirectory and parse all files in it +# 5) if RESULT!="true", initialize hardware using a guess method, +# print an error message and return with exit code 99 +# 6) return with exit code 0 (success) +# + +RESULT="unknown" +ACCESS=="preinit", INCLUDE="preinit" +RESULT=="skip", GOTO="init_end" + +# real ALSA configuration database +CARDINFO{driver}=="HDA-Intel", INCLUDE="hda", GOTO="init_end" +CARDINFO{driver}=="CA0106", INCLUDE="ca0106", GOTO="init_end" +CARDINFO{driver}=="Test", INCLUDE="test", GOTO="init_end" + +LABEL="init_end" +ACCESS=="postinit", INCLUDE="postinit" +RESULT=="true", GOTO="00main_end" +ERROR="Found hardware: \"$cardinfo{driver}\" \"$cardinfo{mixername}\" \"$cardinfo{components}\" \"$attr{subsystem_vendor}\" \"$attr{subsystem_device}\"\n" +ERROR="Hardware is initialized using a generic method\n" +INCLUDE="default" +EXIT="99" + +# +# label identifying end of main file +# + +LABEL="00main_end" diff --git a/alsactl/init/Makefile.am b/alsactl/init/Makefile.am new file mode 100644 index 0000000..64dac41 --- /dev/null +++ b/alsactl/init/Makefile.am @@ -0,0 +1,7 @@ + +init_files = \ + 00main default help info test \ + hda ca0106 +EXTRA_DIST = $(init_files) +alsainitdir = $(datadir)/alsa/init +alsainit_DATA = $(init_files) diff --git a/alsactl/init/ca0106 b/alsactl/init/ca0106 new file mode 100644 index 0000000..45fc833 --- /dev/null +++ b/alsactl/init/ca0106 @@ -0,0 +1,31 @@ +# Configuration for CA0106 driver + +CTL{reset}="mixer" +CTL{name}="Master Playback Volume", CTL{values}="-20dB" +CTL{name}="Master Playback Switch", CTL{values}="on" +CTL{name}="Analog Front Playback Volume", CTL{values}="0dB" +CTL{name}="Analog Front Playback Switch", CTL{values}="on" +CTL{name}="Analog Rear Playback Volume", CTL{values}="0dB" +CTL{name}="Analog Rear Playback Switch", CTL{values}="on" +CTL{name}="Analog Center/LFE Playback Volume", CTL{values}="0dB" +CTL{name}="Analog Center/LFE Playback Switch", CTL{values}="on" +CTL{name}="Analog Side Playback Volume", CTL{values}="0dB" +CTL{name}="Analog Side Playback Switch", CTL{values}="on" +CTL{name}="IEC958 Front Playback Volume", CTL{values}="0dB" +CTL{name}="IEC958 Rear Playback Volume", CTL{values}="0dB" +CTL{name}="IEC958 Center/LFE Playback Volume", CTL{values}="0dB" +# capture +CTL{name}="Analog Source Capture Enum", CTL{value}="Mic" +CTL{name}="Mic Capture Volume", CTL{values}="6dB" +CTL{name}="Shared Mic/Line in Capture Switch", CTL{values}="Mic in" + +# some variants have also AC97 mixer + +CTL{reset}="mixer" +CTL{name}="AC97 Line Capture Volume",PROGRAM!="__ctl_search",GOTO="ac97_end" +CTL{name}="Analog Source Capture Enum", CTL{value}="AC97 in" +CTL{name}="AC97 Mic Capture Switch", "on" +CTL{name}="AC97 Mic Capture Value", "6dB" +LABEL="ac97_end" + +RESULT="true", EXIT="return" diff --git a/alsactl/init/default b/alsactl/init/default new file mode 100644 index 0000000..1c84f3e --- /dev/null +++ b/alsactl/init/default @@ -0,0 +1,284 @@ +# +# Default ALSA volume levels and setting when initialization database fails. +# +# Basic rules are: +# - keep volumes at minimal level, but sound should be hearable +# - enable standard outputs for playback and main microphone for recording +# + +# ************************************************************************** +# playback +# ************************************************************************** + +ENV{ppercent}:="75%" +ENV{cpercent}:="75%" +ENV{pvolume}:="-20dB" +ENV{cvolume}:="12dB" +ENV{has_pmaster_vol}:="false" + +CTL{reset}="mixer" +CTL{name}="Playback Volume",CTL{do_search}=="1", \ + CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +CTL{name}="Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Master Playback Volume",CTL{do_search}=="1", \ + ENV{has_pmaster_vol}:="true", \ + CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +CTL{name}="Master Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Master Front Playback Volume",CTL{do_search}=="1", \ + ENV{has_pmaster_vol}:="true", \ + CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +CTL{name}="Master Front Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Master Digital Playback Volume",CTL{do_search}=="1", \ + CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +CTL{name}="Master Digital Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Line Out Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn line out volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Line Out Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Front Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn front volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Front Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Surround Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn front volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Surround Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Center Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn front volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Center Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="LFE Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn front volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="LFE Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Headphone Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn headphone volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Headphone Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Headphone Playback Volume",CTL{index}="1",PROGRAM!="__ctl_search",\ + GOTO="" +# if master volume control is present, turn headphone volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Headphone Playback Switch",CTL{index}="1",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Speaker Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn speaker volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Speaker Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Front Speaker Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn speaker volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Front Speaker Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Surround Speaker Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn speaker volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Surround Speaker Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Center Speaker Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn speaker volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="Center Speaker Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="LFE Speaker Playback Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn speaker volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" +LABEL="" +CTL{name}="LFE Speaker Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="PC Speaker Playback Volume",CTL{do_search}=="1", \ + CTL{values}="$env{pvolume}",RESULT!="0",CTL{values}="$env{ppercent}" +CTL{name}="PC Speaker Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="PCM Playback Volume",PROGRAM!="__ctl_search", \ + CTL{name}="PCM Volume",PROGRAM!="__ctl_search", GOTO="" +# if master volume control is present, turn PCM volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +# exception - some HDA codecs have shifted dB range +CTL{dBmin}=="-34.50dB",CTL{dBmax}=="12.00dB",CTL{write}=="0dB",GOTO="" +CTL{dBmin}=="-30.00dB",CTL{dBmax}=="0dB",CTL{write}=="0dB",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="75%" +LABEL="" +CTL{name}="PCM Playback Switch",CTL{do_search}=="1", CTL{values}="on" +CTL{name}="PCM Switch",CTL{do_search}=="1",CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="PCM Playback Volume",CTL{index}="1",PROGRAM!="__ctl_search", \ + CTL{name}="PCM Volume",PROGRAM!="__ctl_search",GOTO="" +# if master volume control is present, turn PCM volume to max +ENV{has_pmaster_vol}=="true",CTL{write}=="0dB",GOTO="" +ENV{has_pmaster_vol}=="true",CTL{write}=="100%",GOTO="" +# exception - some HDA codecs have shifted dB range +CTL{dBmin}=="-34.50dB",CTL{dBmax}=="12.00dB",CTL{write}=="0dB",GOTO="" +CTL{dBmin}=="-30.00dB",CTL{dBmax}=="0dB",CTL{write}=="0dB",GOTO="" +CTL{write}!="$env{pvolume}",CTL{values}="75%" +LABEL="" +CTL{name}="PCM Playback Switch",CTL{index}="1",CTL{do_search}=="1", \ + CTL{values}="on" +CTL{name}="PCM Switch",CTL{index}="1",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="DAC Playback Volume",CTL{do_search}=="1", \ + CTL{values}="$env{pvolume}",RESULT!="0",CTL{values}="$env{ppercent}" +CTL{name}="DAC Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Synth Playback Volume",CTL{do_search}=="1", \ + CTL{values}="$env{pvolume}",RESULT!="0",CTL{values}="$env{ppercent}" +CTL{name}="Synth Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Wave Playback Volume",CTL{do_search}=="1", \ + CTL{values}="100%" +CTL{name}="Wave Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Music Playback Volume",CTL{do_search}=="1", \ + CTL{values}="100%" +CTL{name}="Music Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="CD Playback Volume",CTL{do_search}=="1", \ + CTL{values}="0%" +CTL{name}="CD Playback Switch",CTL{do_search}=="1", \ + CTL{values}="off" + +CTL{reset}="mixer" +CTL{name}="Mono Playback Volume",CTL{do_search}=="1", \ + CTL{values}="$env{pvolume}",RESULT!="0",CTL{values}="$env{ppercent}" +CTL{name}="Mono Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="Master Mono Playback Volume",CTL{do_search}=="1", \ + CTL{values}="$env{pvolume}",RESULT!="0",CTL{values}="$env{ppercent}" +CTL{name}="Master Mono Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{reset}="mixer" +CTL{name}="AC97 Playback Volume",CTL{do_search}=="1", \ + CTL{values}="100%" +CTL{name}="AC97 Playback Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +# +# Powermacs +# + +CTL{reset}="mixer" +CTL{name}="DRC Range",CTL{do_search}=="1", \ + CTL{write}!="$env{pvolume}",CTL{values}="$env{ppercent}" + +# ************************************************************************** +# capture +# ************************************************************************** + +CTL{reset}="mixer" +CTL{name}="Capture Volume",CTL{do_search}=="1", \ + CTL{write}!="$env{cvolume}",CTL{values}="$env{cpercent}" +CTL{name}="Capture Switch",CTL{do_search}=="1", \ + CTL{values}="on" + +CTL{name}="Capture Source",PROGRAM!="__ctl_search", GOTO="" +CTL{enums}=="*|Internal Mic|*",CTL{values}="Internal Mic", GOTO="" +CTL{enums}=="*|Mic|*",CTL{values}="Mic" +LABEL="" + +CTL{name}="Input Source",PROGRAM!="__ctl_search", GOTO="" +CTL{enums}=="*|Internal Mic|*",CTL{values}="Internal Mic", GOTO="" +CTL{enums}=="*|Mic|*",CTL{values}="Mic" +LABEL="" + +CTL{name}="Digital Input Source",PROGRAM!="__ctl_search", GOTO="" +CTL{enums}=="*|Digital Mic 1|*",CTL{values}="Digital Mic 1", GOTO="" +CTL{enums}=="*|Mic|*",CTL{values}="Mic" +LABEL="" + +CTL{name}="Mic Boost",CTL{do_search}=="1", CTL{values}="on" +CTL{name}="Internal Mic Boost",CTL{do_search}=="1", CTL{values}="on" diff --git a/alsactl/init/hda b/alsactl/init/hda new file mode 100644 index 0000000..7581095 --- /dev/null +++ b/alsactl/init/hda @@ -0,0 +1,39 @@ +# Configuration for HDA Intel driver (High Definition Audio - Azalia) + +CARDINFO{mixername}=="Realtek ALC880", \ + ATTR{subsystem_vendor}=="0x1025", ATTR{subsystem_device}=="0x0070", \ + GOTO="Acer Travelmate 8100" +CARDINFO{mixername}=="Analog Devices AD1984", \ + ATTR{subsystem_vendor}=="0x17aa", ATTR{subsystem_device}=="0x20ac", \ + GOTO="Lenovo T61" +RESULT="false", EXIT="return" + +LABEL="Acer Travelmate 8100" +# playback +CTL{reset}="mixer" +CTL{name}="Master Playback Volume", CTL{values}="-21dB" +CTL{name}="Master Playback Switch", CTL{values}="on" +CTL{name}="Headphone Playback Switch", CTL{values}="on" +CTL{name}="Front Playback Volume", CTL{values}="-29dB" +CTL{name}="Front Playback Switch", CTL{values}="on" +CTL{name}="PCM Playback Volume", CTL{values}="0dB" +# capture +CTL{name}="Input Source", CTL{value}="Mic" +CTL{name}="Capture Volume", CTL{values}="20dB" +CTL{name}="Capture Switch", CTL{values}="on" +RESULT="true", EXIT="return" + +LABEL="Lenovo T61" +# playback +CTL{reset}="mixer" +CTL{name}="Master Playback Volume", CTL{values}="-13.5dB" +CTL{name}="Master Playback Switch", CTL{values}="on" +CTL{name}="Headphone Playback Switch", CTL{values}="on" +CTL{name}="Speaker Playback Switch", CTL{values}="on" +CTL{name}="PCM Playback Volume", CTL{values}="0dB" +# capture +CTL{name}="Input Source", CTL{value}="Internal Mic" +CTL{name}="Internal Mic Boost", CTL{values}="1" +CTL{name}="Capture Volume", CTL{values}="9dB" +CTL{name}="Capture Switch", CTL{values}="on" +RESULT="true", EXIT="return" diff --git a/alsactl/init/help b/alsactl/init/help new file mode 100644 index 0000000..4f0ba70 --- /dev/null +++ b/alsactl/init/help @@ -0,0 +1,8 @@ +# help page + +PRINT="Available commands (identified by the environment variable CMD):\n\n" +PRINT=" (not set) Do a soundcard initialization\n" +PRINT=" default Do a default (guess method) initialization\n" +PRINT=" help Show this information\n" +PRINT=" info Print all available hardware identification\n" +PRINT=" test Do alsactl utility parser tests\n" diff --git a/alsactl/init/info b/alsactl/init/info new file mode 100644 index 0000000..a4fea19 --- /dev/null +++ b/alsactl/init/info @@ -0,0 +1,22 @@ +# show information about card + +PRINT="CARDINFO:\n" +PRINT=" CARDINFO{id}=\"$CARDINFO{id}\"\n" +PRINT=" CARDINFO{card}=\"$CARDINFO{card}\"\n" +PRINT=" CARDINFO{driver}=\"$CARDINFO{driver}\"\n" +PRINT=" CARDINFO{name}=\"$CARDINFO{name}\"\n" +PRINT=" CARDINFO{longname}=\"$CARDINFO{longname}\"\n" +PRINT=" CARDINFO{mixername}=\"$CARDINFO{mixername}\"\n" +PRINT=" CARDINFO{components}=\"$CARDINFO{components}\"\n" + +# sysfs stuff +PRINT="sysfs:\n" +ATTR{bus}=="*", PRINT=" ATTR{bus}=\"$ATTR{bus}\"\n" +ATTR{class}=="*", PRINT=" ATTR{class}=\"$ATTR{class}\"\n" +ATTR{driver}=="*", PRINT=" ATTR{driver}=\"$ATTR{driver}\"\n" +ATTR{vendor}=="*", PRINT=" ATTR{vendor}=\"$ATTR{vendor}\"\n" +ATTR{device}=="*", PRINT=" ATTR{device}=\"$ATTR{device}\"\n" +ATTR{subsystem_vendor}=="*", \ + PRINT=" ATTR{subsystem_vendor}=\"$ATTR{subsystem_vendor}\"\n" +ATTR{subsystem_device}=="*", \ + PRINT=" ATTR{subsystem_device}=\"$ATTR{subsystem_device}\"\n" diff --git a/alsactl/init/test b/alsactl/init/test new file mode 100644 index 0000000..56659f8 --- /dev/null +++ b/alsactl/init/test @@ -0,0 +1,270 @@ +# Test code +# Just for debugging purposes + +PRINT="Default CTL:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" + +CTL{reset}="mixer" + +PRINT="After CTL{reset}=\"mixer\":\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" + +CTL{numid}="987" +CTL{iface}="sequencer" +CTL{device}="10" +CTL{subdevice}="20" +CTL{name}="Just Test" +CTL{index}="999" + +PRINT="After test sequence:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" + +ERROR="Ignore following error:\n " +PROGRAM="__just_test" + +PRINT="CTL{do_count} test:\n" +CTL{search}="mixer", CTL{name}="*Switch*", \ + PRINT=" *Switch* count result: $ctl{do_count}\n" + +PRINT="__ctl_search test:\n" +CTL{search}="mixer", CTL{name}="*Switch*", PROGRAM!="__ctl_search", GOTO="skip_switch_search" +PRINT=" *Switch 0* search result: $result\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +CTL{search}="mixer", CTL{name}="*Switch*", PROGRAM!="__ctl_search 1", GOTO="skip_switch_search" +PRINT=" *Switch 1* search result: $result\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" + +PRINT="First ten elements:\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 0}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #0:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 1}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #1:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 2}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #2:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 3}!="3", GOTO="skip_first_ten_search" +PRINT=" Element #3:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 4}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #4:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 5}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #5:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 6}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #6:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 7}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #7:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 8}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #8:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +CTL{search}="mixer", CTL{name}="*", CTL{do_search 9}!="1", GOTO="skip_first_ten_search" +PRINT=" Element #9:\n" +PRINT=" CTL{numid}=\"$ctl{numid}\"\n" +PRINT=" CTL{iface}=\"$ctl{iface}\"\n" +PRINT=" CTL{device}=\"$ctl{device}\"\n" +PRINT=" CTL{subdevice}=\"$ctl{subdevice}\"\n" +PRINT=" CTL{name}=\"$ctl{name}\"\n" +PRINT=" CTL{index}=\"$ctl{index}\"\n" +PRINT=" CTL{type}=\"$ctl{type}\"\n" +PRINT=" CTL{attr}=\"$ctl{attr}\"\n" +PRINT=" CTL{owner}=\"$ctl{owner}\"\n" +PRINT=" CTL{count}=\"$ctl{count}\"\n" +PRINT=" CTL{min}=\"$ctl{min}\"\n" +PRINT=" CTL{max}=\"$ctl{max}\"\n" +PRINT=" CTL{step}=\"$ctl{step}\"\n" +PRINT=" CTL{dBmin}=\"$ctl{dBmin}\"\n" +PRINT=" CTL{dBmax}=\"$ctl{dBmax}\"\n" +PRINT=" CTL{items}=\"$ctl{items}\"\n" +PRINT=" CTL{value}=\"$ctl{value}\"\n" +LABEL="skip_first_ten_search" + +PRINT="Elements write test #1:\n", \ + CTL{search}="mixer", CTL{name}="Front Playback Switch", \ + CTL{do_search}=="1", CTL{value}="on,on", \ + PRINT=" result=$result\n" +PRINT="Elements write test #2:\n", \ + CTL{search}="mixer", CTL{name}="Front Playback Volume", \ + CTL{do_search}=="1", CTL{value}="32,32", \ + PRINT=" result=$result\n" +PRINT="Elements write test #3:\n", \ + CTL{search}="mixer", CTL{name}="Front Playback Volume Error", \ + CTL{do_search}=="1", \ +PRINT=" result=$result\n" + +#CTL{reset}="mixer", CTL{name}="Input Source", PRINT="***$ctl{enums}\n" + +PRINT="\nAll tests done..\n" diff --git a/alsactl/init_parse.c b/alsactl/init_parse.c new file mode 100644 index 0000000..267db4b --- /dev/null +++ b/alsactl/init_parse.c @@ -0,0 +1,1787 @@ +/* + * Advanced Linux Sound Architecture Control Program - Parse initialization files + * Copyright (c) by Jaroslav Kysela , + * Greg Kroah-Hartman , + * Kay Sievers + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aconfig.h" +#include "alsactl.h" +#include "list.h" + +#define PATH_SIZE 512 +#define NAME_SIZE 128 +#define EJUSTRETURN 0x7fffffff + +enum key_op { + KEY_OP_UNSET, + KEY_OP_MATCH, + KEY_OP_NOMATCH, + KEY_OP_ADD, + KEY_OP_ASSIGN, + KEY_OP_ASSIGN_FINAL +}; + +struct pair { + char *key; + char *value; + struct pair *next; +}; + +struct space { + struct pair *pairs; + char *rootdir; + char *go_to; + char *program_result; + const char *filename; + int linenum; + int log_run; + int exit_code; + int quit; + unsigned int ctl_id_changed; + snd_hctl_t *ctl_handle; + snd_ctl_card_info_t *ctl_card_info; + snd_ctl_elem_id_t *ctl_id; + snd_ctl_elem_info_t *ctl_info; + snd_ctl_elem_value_t *ctl_value; +}; + +static void Perror(struct space *space, const char *fmt, ...) +{ + va_list arg; + va_start(arg, fmt); + fprintf(stderr, "%s:%i: ", space->filename, space->linenum); + vfprintf(stderr, fmt, arg); + putc('\n', stderr); + va_end(arg); +} + +#include "init_sysdeps.c" +#include "init_utils_string.c" +#include "init_utils_run.c" +#include "init_sysfs.c" + +static void free_space(struct space *space) +{ + struct pair *pair = space->pairs; + struct pair *next = pair; + + while (next) { + pair = next; + next = pair->next; + free(pair->value); + free(pair->key); + free(pair); + } + space->pairs = NULL; + if (space->ctl_value) { + snd_ctl_elem_value_free(space->ctl_value); + space->ctl_value = NULL; + } + if (space->ctl_info) { + snd_ctl_elem_info_free(space->ctl_info); + space->ctl_info = NULL; + } + if (space->ctl_id) { + snd_ctl_elem_id_free(space->ctl_id); + space->ctl_id = NULL; + } + if (space->ctl_card_info) { + snd_ctl_card_info_free(space->ctl_card_info); + space->ctl_card_info = NULL; + } + if (space->ctl_handle) { + snd_hctl_close(space->ctl_handle); + space->ctl_handle = NULL; + } + if (space->rootdir) + free(space->rootdir); + if (space->program_result) + free(space->program_result); + if (space->go_to) + free(space->go_to); + free(space); +} + +static struct pair *value_find(struct space *space, const char *key) +{ + struct pair *pair = space->pairs; + + while (pair && strcmp(pair->key, key) != 0) + pair = pair->next; + return pair; +} + +static int value_set(struct space *space, const char *key, const char *value) +{ + struct pair *pair; + + pair = value_find(space, key); + if (pair) { + free(pair->value); + pair->value = strdup(value); + if (pair->value == NULL) + return -ENOMEM; + } else { + pair = malloc(sizeof(struct pair)); + if (pair == NULL) + return -ENOMEM; + pair->key = strdup(key); + if (pair->key == NULL) { + free(pair); + return -ENOMEM; + } + pair->value = strdup(value); + if (pair->value == NULL) { + free(pair->key); + free(pair); + return -ENOMEM; + } + pair->next = space->pairs; + space->pairs = pair; + } + return 0; +} + +static int init_space(struct space **space, int card) +{ + struct space *res; + char device[16]; + int err; + + res = calloc(1, sizeof(struct space)); + if (res == NULL) + return -ENOMEM; + res->ctl_id_changed = ~0; + res->linenum = -1; + sprintf(device, "hw:%d", card); + err = snd_hctl_open(&res->ctl_handle, device, 0); + if (err < 0) + goto error; + err = snd_hctl_load(res->ctl_handle); + if (err < 0) + goto error; + err = snd_ctl_card_info_malloc(&res->ctl_card_info); + if (err < 0) + goto error; + err = snd_ctl_card_info(snd_hctl_ctl(res->ctl_handle), res->ctl_card_info); + if (err < 0) + goto error; + err = snd_ctl_elem_id_malloc(&res->ctl_id); + if (err < 0) + goto error; + err = snd_ctl_elem_info_malloc(&res->ctl_info); + if (err < 0) + goto error; + err = snd_ctl_elem_value_malloc(&res->ctl_value); + if (err < 0) + goto error; + *space = res; + return 0; + error: + free_space(res); + return err; +} + +static const char *cardinfo_get(struct space *space, const char *attr) +{ + if (strncasecmp(attr, "CARD", 4) == 0) { + static char res[16]; + sprintf(res, "%u", snd_ctl_card_info_get_card(space->ctl_card_info)); + return res; + } + if (strncasecmp(attr, "ID", 2) == 0) + return snd_ctl_card_info_get_id(space->ctl_card_info); + if (strncasecmp(attr, "DRIVER", 6) == 0) + return snd_ctl_card_info_get_driver(space->ctl_card_info); + if (strncasecmp(attr, "NAME", 4) == 0) + return snd_ctl_card_info_get_name(space->ctl_card_info); + if (strncasecmp(attr, "LONGNAME", 8) == 0) + return snd_ctl_card_info_get_longname(space->ctl_card_info); + if (strncasecmp(attr, "MIXERNAME", 9) == 0) + return snd_ctl_card_info_get_mixername(space->ctl_card_info); + if (strncasecmp(attr, "COMPONENTS", 10) == 0) + return snd_ctl_card_info_get_components(space->ctl_card_info); + Perror(space, "unknown cardinfo{} attribute '%s'", attr); + return NULL; +} + +static int check_id_changed(struct space *space, unsigned int what) +{ + snd_hctl_elem_t *elem; + int err; + + if ((space->ctl_id_changed & what & 1) != 0) { + snd_ctl_elem_id_set_numid(space->ctl_id, 0); + elem = snd_hctl_find_elem(space->ctl_handle, space->ctl_id); + if (!elem) + return -ENOENT; + err = snd_hctl_elem_info(elem, space->ctl_info); + if (err == 0) + space->ctl_id_changed &= ~1; + return err; + } + if ((space->ctl_id_changed & what & 2) != 0) { + snd_ctl_elem_id_set_numid(space->ctl_id, 0); + elem = snd_hctl_find_elem(space->ctl_handle, space->ctl_id); + if (!elem) + return -ENOENT; + err = snd_hctl_elem_read(elem, space->ctl_value); + if (err == 0) + space->ctl_id_changed &= ~2; + return err; + } + return 0; +} + +static const char *get_ctl_value(struct space *space) +{ + snd_ctl_elem_type_t type; + unsigned int idx, count; + static char res[1024], tmp[16]; + static const char hex[] = "0123456789abcdef"; + char *pos; + const char *pos1; + + type = snd_ctl_elem_info_get_type(space->ctl_info); + count = snd_ctl_elem_info_get_count(space->ctl_info); + res[0] = '\0'; + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + for (idx = 0; idx < count; idx++) { + if (idx > 0) + strlcat(res, ",", sizeof(res)); + strlcat(res, snd_ctl_elem_value_get_boolean(space->ctl_value, idx) ? "on" : "off", sizeof(res)); + } + break; + case SND_CTL_ELEM_TYPE_INTEGER: + for (idx = 0; idx < count; idx++) { + if (idx > 0) + strlcat(res, ",", sizeof(res)); + snprintf(tmp, sizeof(tmp), "%li", snd_ctl_elem_value_get_integer(space->ctl_value, idx)); + strlcat(res, tmp, sizeof(res)); + } + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + for (idx = 0; idx < count; idx++) { + if (idx > 0) + strlcat(res, ",", sizeof(res)); + snprintf(tmp, sizeof(tmp), "%lli", snd_ctl_elem_value_get_integer64(space->ctl_value, idx)); + strlcat(res, tmp, sizeof(res)); + } + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + for (idx = 0; idx < count; idx++) { + if (idx > 0) + strlcat(res, ",", sizeof(res)); + snprintf(tmp, sizeof(tmp), "%u", snd_ctl_elem_value_get_enumerated(space->ctl_value, idx)); + strlcat(res, tmp, sizeof(res)); + } + break; + case SND_CTL_ELEM_TYPE_BYTES: + case SND_CTL_ELEM_TYPE_IEC958: + if (type == SND_CTL_ELEM_TYPE_IEC958) + count = sizeof(snd_aes_iec958_t); + if (count > (sizeof(res)-1)/2) + count = (sizeof(res)-1/2); + pos = res; + pos1 = snd_ctl_elem_value_get_bytes(space->ctl_value); + while (count > 0) { + idx = *pos1++; + *pos++ = hex[idx >> 4]; + *pos++ = hex[idx & 0x0f]; + count++; + } + *pos++ = '\0'; + break; + default: + Perror(space, "unknown element type '%i'", type); + return NULL; + } + return res; +} + +/* Function to convert from percentage to volume. val = percentage */ +#define convert_prange1(val, min, max) \ + ceil((val) * ((max) - (min)) * 0.01 + (min)) + +static int set_ctl_value(struct space *space, const char *value, int all) +{ + snd_ctl_elem_type_t type; + unsigned int idx, idx2, count, items; + const char *pos, *pos2; + snd_hctl_elem_t *elem; + int val; + long lval; + + type = snd_ctl_elem_info_get_type(space->ctl_info); + count = snd_ctl_elem_info_get_count(space->ctl_info); + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + for (idx = 0; idx < count; idx++) { + while (*value == ' ') + value++; + if (*value == '\0') + goto missing; + val = strncasecmp(value, "true", 4) == 0 || + strncasecmp(value, "yes", 3) == 0 || + strncasecmp(value, "on", 2) == 0 || + strncasecmp(value, "1", 1) == 0; + snd_ctl_elem_value_set_boolean(space->ctl_value, idx, val); + if (all) + continue; + pos = strchr(value, ','); + value = pos ? pos + 1 : value + strlen(value) - 1; + } + break; + case SND_CTL_ELEM_TYPE_INTEGER: + for (idx = 0; idx < count; idx++) { + while (*value == ' ') + value++; + pos = strchr(value, ','); + if (pos) + *(char *)pos = '\0'; + remove_trailing_chars((char *)value, ' '); + items = pos ? (unsigned)(pos - value) : (unsigned)strlen(value); + if (items > 1 && value[items-1] == '%') { + val = convert_prange1(strtol(value, NULL, 0), snd_ctl_elem_info_get_min(space->ctl_info), snd_ctl_elem_info_get_max(space->ctl_info)); + snd_ctl_elem_value_set_integer(space->ctl_value, idx, val); + } else if (items > 2 && value[items-2] == 'd' && value[items-1] == 'B') { + val = strtol(value, NULL, 0) * 100; + if ((pos2 = strchr(value, '.')) != NULL) { + if (isdigit(*(pos2-1)) && isdigit(*(pos2-2))) { + if (val < 0) + val -= strtol(pos2 + 1, NULL, 0); + else + val += strtol(pos2 + 1, NULL, 0); + } else if (isdigit(*(pos2-1))) { + if (val < 0) + val -= strtol(pos2 + 1, NULL, 0) * 10; + else + val += strtol(pos2 + 1, NULL, 0) * 10; + } + } + val = snd_ctl_convert_from_dB(snd_hctl_ctl(space->ctl_handle), space->ctl_id, val, &lval, -1); + if (val < 0) { + dbg("unable to convert dB value '%s' to internal integer range", value); + return val; + } + snd_ctl_elem_value_set_integer(space->ctl_value, idx, lval); + } else { + snd_ctl_elem_value_set_integer(space->ctl_value, idx, strtol(value, NULL, 0)); + } + if (all) + continue; + value = pos ? pos + 1 : value + strlen(value) - 1; + } + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + for (idx = 0; idx < count; idx++) { + while (*value == ' ') + value++; + snd_ctl_elem_value_set_integer64(space->ctl_value, idx, strtoll(value, NULL, 0)); + if (all) + continue; + pos = strchr(value, ','); + value = pos ? pos + 1 : value + strlen(value) - 1; + } + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + for (idx = 0; idx < count; idx++) { + while (*value == ' ') + value++; + pos = strchr(value, ','); + if (isdigit(value[0]) || value[0] == '-') { + snd_ctl_elem_value_set_enumerated(space->ctl_value, idx, strtol(value, NULL, 0)); + } else { + if (pos) + *(char *)pos = '\0'; + remove_trailing_chars((char *)value, ' '); + items = snd_ctl_elem_info_get_items(space->ctl_info); + for (idx2 = 0; idx2 < items; idx2++) { + snd_ctl_elem_info_set_item(space->ctl_info, idx2); + elem = snd_hctl_find_elem(space->ctl_handle, space->ctl_id); + if (elem == NULL) + return -ENOENT; + val = snd_hctl_elem_info(elem, space->ctl_info); + if (val < 0) + return val; + if (strcasecmp(snd_ctl_elem_info_get_item_name(space->ctl_info), value) == 0) { + snd_ctl_elem_value_set_enumerated(space->ctl_value, idx, idx2); + break; + } + } + if (idx2 >= items) { + Perror(space, "wrong enum identifier '%s'", value); + return -EINVAL; + } + } + if (all) + continue; + value = pos ? pos + 1 : value + strlen(value) - 1; + } + break; + case SND_CTL_ELEM_TYPE_BYTES: + case SND_CTL_ELEM_TYPE_IEC958: + if (type == SND_CTL_ELEM_TYPE_IEC958) + count = sizeof(snd_aes_iec958_t); + while (*value == ' ') + value++; + if (strlen(value) != count * 2) { + Perror(space, "bad ctl value hexa length (should be %u bytes)", count); + return -EINVAL; + } + for (idx = 0; idx < count; idx += 2) { + int nibble1 = hextodigit(*(value++)); + int nibble2 = hextodigit(*(value++)); + if (nibble1 < 0 || nibble2 < 0) { + Perror(space, "bad ctl hexa value"); + return -EINVAL; + } + val = (nibble1 << 4) | nibble2; + snd_ctl_elem_value_set_byte(space->ctl_value, idx, val); + } + break; + default: + Perror(space, "unknown element type '%i'", type); + return -EINVAL; + } + return 0; + missing: + Perror(space, "missing some ctl values (line %i)", space->linenum); + return -EINVAL; +} + +static int do_match(const char *key, enum key_op op, + const char *key_value, const char *value) +{ + int match; + + if (value == NULL) + return 0; + dbg("match %s '%s' <-> '%s'", key, key_value, value); + match = fnmatch(key_value, value, 0) == 0; + if (match && op == KEY_OP_MATCH) { + dbg("%s is true (matching value)", key); + return 1; + } + if (!match && op == KEY_OP_NOMATCH) { + dbg("%s is true (non-matching value)", key); + return 1; + } + dbg("%s is false", key); + return 0; +} + +static int ctl_match(snd_ctl_elem_id_t *pattern, snd_ctl_elem_id_t *id) +{ + if ((int)snd_ctl_elem_id_get_interface(pattern) != -1 && + snd_ctl_elem_id_get_interface(pattern) != snd_ctl_elem_id_get_interface(id)) + return 0; + if ((int)snd_ctl_elem_id_get_device(pattern) != -1 && + snd_ctl_elem_id_get_device(pattern) != snd_ctl_elem_id_get_device(id)) + return 0; + if ((int)snd_ctl_elem_id_get_subdevice(pattern) != -1 && + snd_ctl_elem_id_get_subdevice(pattern) != snd_ctl_elem_id_get_subdevice(id)) + return 0; + if ((int)snd_ctl_elem_id_get_index(pattern) != -1 && + snd_ctl_elem_id_get_index(pattern) != snd_ctl_elem_id_get_index(id)) + return 0; + if (fnmatch(snd_ctl_elem_id_get_name(pattern), snd_ctl_elem_id_get_name(id), 0) != 0) + return 0; + return 1; +} + +static const char *elemid_get(struct space *space, const char *attr) +{ + long long val; + snd_ctl_elem_type_t type; + static char res[256]; + + if (strncasecmp(attr, "numid", 5) == 0) { + val = snd_ctl_elem_id_get_numid(space->ctl_id); + goto value; + } + if (strncasecmp(attr, "iface", 5) == 0 || + strncasecmp(attr, "interface", 9) == 0) + return snd_ctl_elem_iface_name(snd_ctl_elem_id_get_interface(space->ctl_id)); + if (strncasecmp(attr, "device", 6) == 0) { + val = snd_ctl_elem_id_get_device(space->ctl_id); + goto value; + } + if (strncasecmp(attr, "subdev", 6) == 0) { + val = snd_ctl_elem_id_get_subdevice(space->ctl_id); + goto value; + } + if (strncasecmp(attr, "name", 4) == 0) + return snd_ctl_elem_id_get_name(space->ctl_id); + if (strncasecmp(attr, "index", 5) == 0) { + val = snd_ctl_elem_id_get_index(space->ctl_id); + goto value; + } + if (strncasecmp(attr, "type", 4) == 0) { + if (check_id_changed(space, 1)) + return NULL; + return snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(space->ctl_info)); + } + if (strncasecmp(attr, "attr", 4) == 0) { + if (check_id_changed(space, 1)) + return NULL; + res[0] = '\0'; + if (snd_ctl_elem_info_is_readable(space->ctl_info)) + strcat(res, "r"); + if (snd_ctl_elem_info_is_writable(space->ctl_info)) + strcat(res, "w"); + if (snd_ctl_elem_info_is_volatile(space->ctl_info)) + strcat(res, "v"); + if (snd_ctl_elem_info_is_inactive(space->ctl_info)) + strcat(res, "i"); + if (snd_ctl_elem_info_is_locked(space->ctl_info)) + strcat(res, "l"); + if (snd_ctl_elem_info_is_tlv_readable(space->ctl_info)) + strcat(res, "R"); + if (snd_ctl_elem_info_is_tlv_writable(space->ctl_info)) + strcat(res, "W"); + if (snd_ctl_elem_info_is_tlv_commandable(space->ctl_info)) + strcat(res, "C"); + if (snd_ctl_elem_info_is_owner(space->ctl_info)) + strcat(res, "o"); + if (snd_ctl_elem_info_is_user(space->ctl_info)) + strcat(res, "u"); + return res; + } + if (strncasecmp(attr, "owner", 5) == 0) { + if (check_id_changed(space, 1)) + return NULL; + val = snd_ctl_elem_info_get_owner(space->ctl_info); + goto value; + } + if (strncasecmp(attr, "count", 5) == 0) { + if (check_id_changed(space, 1)) + return NULL; + val = snd_ctl_elem_info_get_count(space->ctl_info); + goto value; + } + if (strncasecmp(attr, "min", 3) == 0) { + if (check_id_changed(space, 1)) + return NULL; + type = snd_ctl_elem_info_get_type(space->ctl_info); + if (type == SND_CTL_ELEM_TYPE_INTEGER64) + val = snd_ctl_elem_info_get_min64(space->ctl_info); + else if (type == SND_CTL_ELEM_TYPE_INTEGER) + val = snd_ctl_elem_info_get_min(space->ctl_info); + else + goto empty; + goto value; + } + if (strncasecmp(attr, "max", 3) == 0) { + if (check_id_changed(space, 1)) + return NULL; + type = snd_ctl_elem_info_get_type(space->ctl_info); + if (type == SND_CTL_ELEM_TYPE_INTEGER64) + val = snd_ctl_elem_info_get_max64(space->ctl_info); + else if (type == SND_CTL_ELEM_TYPE_INTEGER) + val = snd_ctl_elem_info_get_max(space->ctl_info); + else + goto empty; + goto value; + } + if (strncasecmp(attr, "step", 3) == 0) { + if (check_id_changed(space, 1)) + return NULL; + type = snd_ctl_elem_info_get_type(space->ctl_info); + if (type == SND_CTL_ELEM_TYPE_INTEGER64) + val = snd_ctl_elem_info_get_step64(space->ctl_info); + else if (type == SND_CTL_ELEM_TYPE_INTEGER) + val = snd_ctl_elem_info_get_step(space->ctl_info); + else + goto empty; + goto value; + } + if (strncasecmp(attr, "items", 5) == 0) { + if (check_id_changed(space, 1)) + return NULL; + if (snd_ctl_elem_info_get_type(space->ctl_info) == SND_CTL_ELEM_TYPE_ENUMERATED) + val = snd_ctl_elem_info_get_items(space->ctl_info); + else { + empty: + res[0] = '\0'; + return res; + } + goto value; + } + if (strncasecmp(attr, "value", 5) == 0) { + if (check_id_changed(space, 3)) + return NULL; + return get_ctl_value(space); + } + if (strncasecmp(attr, "dBmin", 5) == 0) { + long min, max; + if (check_id_changed(space, 1)) + return NULL; + if (snd_ctl_get_dB_range(snd_hctl_ctl(space->ctl_handle), space->ctl_id, &min, &max) < 0) + goto empty; + val = min; +dbvalue: + sprintf(res, "%li.%02lidB", (long)(val / 100), labs(val % 100)); + return res; + } + if (strncasecmp(attr, "dBmax", 5) == 0) { + long min, max; + if (check_id_changed(space, 1)) + return NULL; + if (snd_ctl_get_dB_range(snd_hctl_ctl(space->ctl_handle), space->ctl_id, &min, &max) < 0) + goto empty; + val = max; + goto dbvalue; + } + if (strncasecmp(attr, "enums", 5) == 0) { + unsigned int idx, items; + snd_hctl_elem_t *elem; + if (check_id_changed(space, 1)) + return NULL; + if (snd_ctl_elem_info_get_type(space->ctl_info) != SND_CTL_ELEM_TYPE_ENUMERATED) + goto empty; + items = snd_ctl_elem_info_get_items(space->ctl_info); + strcpy(res, "|"); + for (idx = 0; idx < items; idx++) { + snd_ctl_elem_info_set_item(space->ctl_info, idx); + elem = snd_hctl_find_elem(space->ctl_handle, space->ctl_id); + if (elem == NULL) + break; + if (snd_hctl_elem_info(elem, space->ctl_info) < 0) + break; + strlcat(res, snd_ctl_elem_info_get_item_name(space->ctl_info), sizeof(res)); + strlcat(res, "|", sizeof(res)); + } + return res; + } + if (strncasecmp(attr, "do_search", 9) == 0) { + int err, index = 0; + snd_hctl_elem_t *elem; + snd_ctl_elem_id_t *id; + char *pos = strchr(attr, ' '); + if (pos) + index = strtol(pos, NULL, 0); + err = snd_ctl_elem_id_malloc(&id); + if (err < 0) + return NULL; + elem = snd_hctl_first_elem(space->ctl_handle); + while (elem) { + snd_hctl_elem_get_id(elem, id); + if (!ctl_match(space->ctl_id, id)) + goto next_search; + if (index > 0) { + index--; + goto next_search; + } + strcpy(res, "1"); + snd_ctl_elem_id_copy(space->ctl_id, id); + snd_ctl_elem_id_free(id); + dbg("do_ctl_search found a control"); + return res; + next_search: + elem = snd_hctl_elem_next(elem); + } + snd_ctl_elem_id_free(id); + strcpy(res, "0"); + return res; + } + if (strncasecmp(attr, "do_count", 8) == 0) { + int err, index = 0; + snd_hctl_elem_t *elem; + snd_ctl_elem_id_t *id; + err = snd_ctl_elem_id_malloc(&id); + if (err < 0) + return NULL; + elem = snd_hctl_first_elem(space->ctl_handle); + while (elem) { + snd_hctl_elem_get_id(elem, id); + if (ctl_match(space->ctl_id, id)) + index++; + elem = snd_hctl_elem_next(elem); + } + snd_ctl_elem_id_free(id); + sprintf(res, "%d", index); + dbg("do_ctl_count found %s controls", res); + return res; + } + Perror(space, "unknown ctl{} attribute '%s'", attr); + return NULL; + value: + sprintf(res, "%lli", val); + return res; +} + +static int elemid_set(struct space *space, const char *attr, const char *value) +{ + unsigned int val; + void (*fcn)(snd_ctl_elem_id_t *, unsigned int); + snd_ctl_elem_iface_t iface; + int err; + + if (strncasecmp(attr, "numid", 5) == 0) { + fcn = snd_ctl_elem_id_set_numid; + goto value; + } + if (strncasecmp(attr, "iface", 5) == 0 || + strncasecmp(attr, "interface", 9) == 0 || + strncasecmp(attr, "reset", 5) == 0 || + strncasecmp(attr, "search", 6) == 0) { + if (strlen(value) == 0 && strncasecmp(attr, "search", 6) == 0) { + iface = 0; + goto search; + } + for (iface = 0; iface <= SND_CTL_ELEM_IFACE_LAST; iface++) { + if (strcasecmp(value, snd_ctl_elem_iface_name(iface)) == 0) { + if (strncasecmp(attr, "reset", 5) == 0) + snd_ctl_elem_id_clear(space->ctl_id); + if (strncasecmp(attr, "search", 5) == 0) { + search: + snd_ctl_elem_id_clear(space->ctl_id); + /* -1 means all */ + snd_ctl_elem_id_set_interface(space->ctl_id, -1); + snd_ctl_elem_id_set_device(space->ctl_id, -1); + snd_ctl_elem_id_set_subdevice(space->ctl_id, -1); + snd_ctl_elem_id_set_name(space->ctl_id, "*"); + snd_ctl_elem_id_set_index(space->ctl_id, -1); + if (strlen(value) == 0) + return 0; + } + snd_ctl_elem_id_set_interface(space->ctl_id, iface); + space->ctl_id_changed = ~0; + return 0; + } + } + Perror(space, "unknown control interface name '%s'", value); + return -EINVAL; + } + if (strncasecmp(attr, "device", 6) == 0) { + fcn = snd_ctl_elem_id_set_device; + goto value; + } + if (strncasecmp(attr, "subdev", 6) == 0) { + fcn = snd_ctl_elem_id_set_subdevice; + goto value; + } + if (strncasecmp(attr, "name", 4) == 0) { + snd_ctl_elem_id_set_name(space->ctl_id, value); + space->ctl_id_changed = ~0; + return 0; + } + if (strncasecmp(attr, "index", 5) == 0) { + fcn = snd_ctl_elem_id_set_index; + goto value; + } + if (strncasecmp(attr, "values", 6) == 0 || + strncasecmp(attr, "value", 5) == 0) { + err = check_id_changed(space, 1); + if (err < 0) { + Perror(space, "control element not found"); + return err; + } + err = set_ctl_value(space, value, strncasecmp(attr, "values", 6) == 0); + if (err < 0) { + space->ctl_id_changed |= 2; + } else { + space->ctl_id_changed &= ~2; + snd_ctl_elem_value_set_id(space->ctl_value, space->ctl_id); + err = snd_ctl_elem_write(snd_hctl_ctl(space->ctl_handle), space->ctl_value); + if (err < 0) { + Perror(space, "value write error: %s", snd_strerror(err)); + return err; + } + } + return err; + } + Perror(space, "unknown CTL{} attribute '%s'", attr); + return -EINVAL; + value: + val = (unsigned int)strtol(value, NULL, 0); + fcn(space->ctl_id, val); + space->ctl_id_changed = ~0; + return 0; +} + +static int get_key(char **line, char **key, enum key_op *op, char **value) +{ + char *linepos; + char *temp; + + linepos = *line; + if (linepos == NULL && linepos[0] == '\0') + return -EINVAL; + + /* skip whitespace */ + while (isspace(linepos[0]) || linepos[0] == ',') + linepos++; + + /* get the key */ + if (linepos[0] == '\0') + return -EINVAL; + *key = linepos; + + while (1) { + linepos++; + if (linepos[0] == '\0') + return -1; + if (isspace(linepos[0])) + break; + if (linepos[0] == '=') + break; + if (linepos[0] == '+') + break; + if (linepos[0] == '!') + break; + if (linepos[0] == ':') + break; + } + + /* remember end of key */ + temp = linepos; + + /* skip whitespace after key */ + while (isspace(linepos[0])) + linepos++; + if (linepos[0] == '\0') + return -EINVAL; + + /* get operation type */ + if (linepos[0] == '=' && linepos[1] == '=') { + *op = KEY_OP_MATCH; + linepos += 2; + dbg("operator=match"); + } else if (linepos[0] == '!' && linepos[1] == '=') { + *op = KEY_OP_NOMATCH; + linepos += 2; + dbg("operator=nomatch"); + } else if (linepos[0] == '+' && linepos[1] == '=') { + *op = KEY_OP_ADD; + linepos += 2; + dbg("operator=add"); + } else if (linepos[0] == '=') { + *op = KEY_OP_ASSIGN; + linepos++; + dbg("operator=assign"); + } else if (linepos[0] == ':' && linepos[1] == '=') { + *op = KEY_OP_ASSIGN_FINAL; + linepos += 2; + dbg("operator=assign_final"); + } else + return -EINVAL; + + /* terminate key */ + temp[0] = '\0'; + dbg("key='%s'", *key); + + /* skip whitespace after operator */ + while (isspace(linepos[0])) + linepos++; + if (linepos[0] == '\0') + return -EINVAL; + + /* get the value*/ + if (linepos[0] != '"') + return -EINVAL; + linepos++; + *value = linepos; + + while (1) { + temp = strchr(linepos, '"'); + if (temp && temp[-1] == '\\') { + linepos = temp + 1; + continue; + } + break; + } + if (!temp) + return -EINVAL; + temp[0] = '\0'; + temp++; + dbg("value='%s'", *value); + + /* move line to next key */ + *line = temp; + + return 0; +} + +/* extract possible KEY{attr} */ +static char *get_key_attribute(struct space *space, char *str, char *res, size_t ressize) +{ + char *pos; + char *attr; + + attr = strchr(str, '{'); + if (attr != NULL) { + attr++; + pos = strchr(attr, '}'); + if (pos == NULL) { + Perror(space, "missing closing brace for format"); + return NULL; + } + pos[0] = '\0'; + strlcpy(res, attr, ressize); + pos[0] = '}'; + dbg("attribute='%s'", res); + return res; + } + + return NULL; +} + +/* extract possible {attr} and move str behind it */ +static char *get_format_attribute(struct space *space, char **str) +{ + char *pos; + char *attr = NULL; + + if (*str[0] == '{') { + pos = strchr(*str, '}'); + if (pos == NULL) { + Perror(space, "missing closing brace for format"); + return NULL; + } + pos[0] = '\0'; + attr = *str+1; + *str = pos+1; + dbg("attribute='%s', str='%s'", attr, *str); + } + return attr; +} + +/* extract possible format length and move str behind it*/ +static int get_format_len(struct space *space, char **str) +{ + int num; + char *tail; + + if (isdigit(*str[0])) { + num = (int) strtoul(*str, &tail, 10); + if (num > 0) { + *str = tail; + dbg("format length=%i", num); + return num; + } else { + Perror(space, "format parsing error '%s'", *str); + } + } + return -1; +} + +static void apply_format(struct space *space, char *string, size_t maxsize) +{ + char temp[PATH_SIZE]; + char temp2[PATH_SIZE]; + char *head, *tail, *pos, *cpos, *attr, *rest; + struct pair *pair; + int len; + int i; + int count; + enum subst_type { + SUBST_UNKNOWN, + SUBST_CARDINFO, + SUBST_CTL, + SUBST_RESULT, + SUBST_ATTR, + SUBST_SYSFSROOT, + SUBST_ENV, + SUBST_CONFIG, + }; + static const struct subst_map { + char *name; + char fmt; + enum subst_type type; + } map[] = { + { .name = "cardinfo", .fmt = 'i', .type = SUBST_CARDINFO }, + { .name = "ctl", .fmt = 'C', .type = SUBST_CTL }, + { .name = "result", .fmt = 'c', .type = SUBST_RESULT }, + { .name = "attr", .fmt = 's', .type = SUBST_ATTR }, + { .name = "sysfsroot", .fmt = 'r', .type = SUBST_SYSFSROOT }, + { .name = "env", .fmt = 'E', .type = SUBST_ENV }, + { .name = "config", .fmt = 'g', .type = SUBST_CONFIG }, + { NULL, '\0', 0 } + }; + enum subst_type type; + const struct subst_map *subst; + + head = string; + while (1) { + len = -1; + while (head[0] != '\0') { + if (head[0] == '$') { + /* substitute named variable */ + if (head[1] == '\0') + break; + if (head[1] == '$') { + strlcpy(temp, head+2, sizeof(temp)); + strlcpy(head+1, temp, maxsize); + head++; + continue; + } + head[0] = '\0'; + for (subst = map; subst->name; subst++) { + if (strncasecmp(&head[1], subst->name, strlen(subst->name)) == 0) { + type = subst->type; + tail = head + strlen(subst->name)+1; + dbg("will substitute format name '%s'", subst->name); + goto found; + } + } + } else if (head[0] == '%') { + /* substitute format char */ + if (head[1] == '\0') + break; + if (head[1] == '%') { + strlcpy(temp, head+2, sizeof(temp)); + strlcpy(head+1, temp, maxsize); + head++; + continue; + } + head[0] = '\0'; + tail = head+1; + len = get_format_len(space, &tail); + for (subst = map; subst->name; subst++) { + if (tail[0] == subst->fmt) { + type = subst->type; + tail++; + dbg("will substitute format char '%c'", subst->fmt); + goto found; + } + } + } + head++; + } + break; +found: + attr = get_format_attribute(space, &tail); + strlcpy(temp, tail, sizeof(temp)); + dbg("format=%i, string='%s', tail='%s'", type ,string, tail); + + switch (type) { + case SUBST_CARDINFO: + if (attr == NULL) + Perror(space, "missing identification parametr for cardinfo"); + else { + const char *value = cardinfo_get(space, attr); + if (value == NULL) + break; + strlcat(string, value, maxsize); + dbg("substitute cardinfo{%s} '%s'", attr, value); + } + break; + case SUBST_CTL: + if (attr == NULL) + Perror(space, "missing identification parametr for ctl"); + else { + const char *value = elemid_get(space, attr); + if (value == NULL) + break; + strlcat(string, value, maxsize); + dbg("substitute ctl{%s} '%s'", attr, value); + } + break; + case SUBST_RESULT: + if (space->program_result == NULL) + break; + /* get part part of the result string */ + i = 0; + if (attr != NULL) + i = strtoul(attr, &rest, 10); + if (i > 0) { + dbg("request part #%d of result string", i); + cpos = space->program_result; + while (--i) { + while (cpos[0] != '\0' && !isspace(cpos[0])) + cpos++; + while (isspace(cpos[0])) + cpos++; + } + if (i > 0) { + Perror(space, "requested part of result string not found"); + break; + } + strlcpy(temp2, cpos, sizeof(temp2)); + /* %{2+}c copies the whole string from the second part on */ + if (rest[0] != '+') { + cpos = strchr(temp2, ' '); + if (cpos) + cpos[0] = '\0'; + } + strlcat(string, temp2, maxsize); + dbg("substitute part of result string '%s'", temp2); + } else { + strlcat(string, space->program_result, maxsize); + dbg("substitute result string '%s'", space->program_result); + } + break; + case SUBST_ATTR: + if (attr == NULL) + Perror(space, "missing file parameter for attr"); + else { + const char *value = NULL; + size_t size; + + pair = value_find(space, "sysfs_device"); + if (pair == NULL) + break; + value = sysfs_attr_get_value(pair->value, attr); + + if (value == NULL) + break; + + /* strip trailing whitespace and replace untrusted characters of sysfs value */ + size = strlcpy(temp2, value, sizeof(temp2)); + if (size >= sizeof(temp2)) + size = sizeof(temp2)-1; + while (size > 0 && isspace(temp2[size-1])) + temp2[--size] = '\0'; + count = replace_untrusted_chars(temp2); + if (count > 0) + Perror(space, "%i untrusted character(s) replaced" , count); + strlcat(string, temp2, maxsize); + dbg("substitute sysfs value '%s'", temp2); + } + break; + case SUBST_SYSFSROOT: + strlcat(string, sysfs_path, maxsize); + dbg("substitute sysfs_path '%s'", sysfs_path); + break; + case SUBST_ENV: + if (attr == NULL) { + dbg("missing attribute"); + break; + } + pos = getenv(attr); + if (pos == NULL) { + dbg("env '%s' not available", attr); + break; + } + dbg("substitute env '%s=%s'", attr, pos); + strlcat(string, pos, maxsize); + break; + case SUBST_CONFIG: + if (attr == NULL) { + dbg("missing attribute"); + break; + } + pair = value_find(space, attr); + if (pair == NULL) + break; + strlcat(string, pair->value, maxsize); + break; + default: + Perror(space, "unknown substitution type=%i", type); + break; + } + /* possibly truncate to format-char specified length */ + if (len != -1) { + head[len] = '\0'; + dbg("truncate to %i chars, subtitution string becomes '%s'", len, head); + } + strlcat(string, temp, maxsize); + } + /* unescape strings */ + head = tail = string; + while (*head != '\0') { + if (*head == '\\') { + head++; + if (*head == '\0') + break; + switch (*head) { + case 'a': *tail++ = '\a'; break; + case 'b': *tail++ = '\b'; break; + case 'n': *tail++ = '\n'; break; + case 'r': *tail++ = '\r'; break; + case 't': *tail++ = '\t'; break; + case 'v': *tail++ = '\v'; break; + case '\\': *tail++ = '\\'; break; + default: *tail++ = *head; break; + } + head++; + continue; + } + if (*head) + *tail++ = *head++; + } + *tail = 0; +} + +static +int run_program1(struct space *space, + const char *command0, char *result, + size_t ressize, size_t *reslen ATTRIBUTE_UNUSED, + int log ATTRIBUTE_UNUSED) +{ + if (strncmp(command0, "__ctl_search", 12) == 0) { + const char *res = elemid_get(space, "do_search"); + if (res == NULL || strcmp(res, "1") != 0) + return EXIT_FAILURE; + return EXIT_SUCCESS; + } + if (strncmp(command0, "__ctl_count", 11) == 0) { + const char *res = elemid_get(space, "do_count"); + if (res == NULL || strcmp(res, "0") == 0) + return EXIT_FAILURE; + strlcpy(result, res, ressize); + return EXIT_SUCCESS; + } + Perror(space, "unknown buildin command '%s'", command0); + return EXIT_FAILURE; +} + +static int parse(struct space *space, const char *filename); + +static char *new_root_dir(const char *filename) +{ + char *res, *tmp; + + res = strdup(filename); + if (res) { + tmp = strrchr(res, '/'); + if (tmp) + *tmp = '\0'; + } + dbg("new_root_dir '%s' '%s'", filename, res); + return res; +} + +/* return non-zero if the file name has the extension ".conf" */ +static int conf_name_filter(const struct dirent *d) +{ + char *ext = strrchr(d->d_name, '.'); + return ext && !strcmp(ext, ".conf"); +} + +static int parse_line(struct space *space, char *line, size_t linesize ATTRIBUTE_UNUSED) +{ + char *linepos; + char *key, *value, *attr, *temp; + struct pair *pair; + enum key_op op; + int err = 0, count; + char string[PATH_SIZE]; + char result[PATH_SIZE]; + + linepos = line; + while (*linepos != '\0') { + op = KEY_OP_UNSET; + + err = get_key(&linepos, &key, &op, &value); + if (err < 0) + goto invalid; + + if (strncasecmp(key, "LABEL", 5) == 0) { + if (op != KEY_OP_ASSIGN) { + Perror(space, "invalid LABEL operation"); + goto invalid; + } + if (space->go_to && strcmp(space->go_to, value) == 0) { + free(space->go_to); + space->go_to = NULL; + } + continue; + } + + if (space->go_to) { + dbg("skip (GOTO '%s')", space->go_to); + break; /* not for us */ + } + + if (strncasecmp(key, "CTL{", 4) == 0) { + attr = get_key_attribute(space, key + 3, string, sizeof(string)); + if (attr == NULL) { + Perror(space, "error parsing CTL attribute"); + goto invalid; + } + if (op == KEY_OP_ASSIGN) { + strlcpy(result, value, sizeof(result)); + apply_format(space, result, sizeof(result)); + dbg("ctl assign: '%s' '%s'", value, attr); + err = elemid_set(space, attr, result); + if (space->program_result) { + free(space->program_result); + space->program_result = NULL; + } + snprintf(string, sizeof(string), "%i", err); + space->program_result = strdup(string); + err = 0; + if (space->program_result == NULL) + break; + } else if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + if (strncmp(attr, "write", 5) == 0) { + strlcpy(result, value, sizeof(result)); + apply_format(space, result, sizeof(result)); + dbg("ctl write: '%s' '%s'", value, attr); + err = elemid_set(space, "values", result); + if (err == 0 && op == KEY_OP_NOMATCH) + break; + if (err != 0 && op == KEY_OP_MATCH) + break; + } else { + temp = (char *)elemid_get(space, attr); + dbg("ctl match: '%s' '%s' '%s'", attr, value, temp); + if (!do_match(key, op, value, temp)) + break; + } + } else { + Perror(space, "invalid CTL{} operation"); + goto invalid; + } + continue; + } + if (strcasecmp(key, "RESULT") == 0) { + if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + if (!do_match(key, op, value, space->program_result)) + break; + } else if (op == KEY_OP_ASSIGN) { + if (space->program_result) { + free(space->program_result); + space->program_result = NULL; + } + strlcpy(string, value, sizeof(string)); + apply_format(space, string, sizeof(string)); + space->program_result = strdup(string); + if (space->program_result == NULL) + break; + } else { + Perror(space, "invalid RESULT operation"); + goto invalid; + } + continue; + } + if (strcasecmp(key, "PROGRAM") == 0) { + if (op == KEY_OP_UNSET) + continue; + strlcpy(string, value, sizeof(string)); + apply_format(space, string, sizeof(string)); + if (space->program_result) { + free(space->program_result); + space->program_result = NULL; + } + if (run_program(space, string, result, sizeof(result), NULL, space->log_run) != 0) { + dbg("PROGRAM '%s' is false", string); + if (op != KEY_OP_NOMATCH) + break; + } else { + remove_trailing_chars(result, '\n'); + count = replace_untrusted_chars(result); + if (count) + info("%i untrusted character(s) replaced", count); + dbg("PROGRAM '%s' result is '%s'", string, result); + space->program_result = strdup(result); + if (space->program_result == NULL) + break; + dbg("PROGRAM returned successful"); + if (op == KEY_OP_NOMATCH) + break; + } + dbg("PROGRAM key is true"); + continue; + } + if (strncasecmp(key, "CARDINFO{", 9) == 0) { + attr = get_key_attribute(space, key + 8, string, sizeof(string)); + if (attr == NULL) { + Perror(space, "error parsing CARDINFO attribute"); + goto invalid; + } + if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + dbg("cardinfo: '%s' '%s'", value, attr); + temp = (char *)cardinfo_get(space, attr); + if (!do_match(key, op, value, temp)) + break; + } else { + Perror(space, "invalid CARDINFO{} operation"); + goto invalid; + } + continue; + } + if (strncasecmp(key, "ATTR{", 5) == 0) { + attr = get_key_attribute(space, key + 4, string, sizeof(string)); + if (attr == NULL) { + Perror(space, "error parsing ATTR attribute"); + goto invalid; + } + if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + pair = value_find(space, "sysfs_device"); + if (pair == NULL) + break; + dbg("sysfs_attr: '%s' '%s'", pair->value, attr); + temp = sysfs_attr_get_value(pair->value, attr); + if (!do_match(key, op, value, temp)) + break; + } else { + Perror(space, "invalid ATTR{} operation"); + goto invalid; + } + continue; + } + if (strncasecmp(key, "ENV{", 4) == 0) { + attr = get_key_attribute(space, key + 3, string, sizeof(string)); + if (attr == NULL) { + Perror(space, "error parsing ENV attribute"); + goto invalid; + } + if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + temp = getenv(attr); + dbg("env: '%s' '%s'", attr, temp); + if (!do_match(key, op, value, temp)) + break; + } else if (op == KEY_OP_ASSIGN || + op == KEY_OP_ASSIGN_FINAL) { + strlcpy(result, value, sizeof(result)); + apply_format(space, result, sizeof(result)); + dbg("env set: '%s' '%s'", attr, result); + if (setenv(attr, result, op == KEY_OP_ASSIGN_FINAL)) + break; + } else { + Perror(space, "invalid ENV{} operation"); + goto invalid; + } + continue; + } + if (strcasecmp(key, "GOTO") == 0) { + if (op != KEY_OP_ASSIGN) { + Perror(space, "invalid GOTO operation"); + goto invalid; + } + space->go_to = strdup(value); + if (space->go_to == NULL) { + err = -ENOMEM; + break; + } + continue; + } + if (strcasecmp(key, "INCLUDE") == 0) { + char *rootdir, *go_to; + const char *filename; + struct stat st; + int linenum; + if (op != KEY_OP_ASSIGN) { + Perror(space, "invalid INCLUDE operation"); + goto invalid; + } + if (value[0] == '/') + strlcpy(string, value, sizeof(string)); + else { + strlcpy(string, space->rootdir, sizeof(string)); + strlcat(string, "/", sizeof(string)); + strlcat(string, value, sizeof(string)); + } + rootdir = space->rootdir; + go_to = space->go_to; + filename = space->filename; + linenum = space->linenum; + if (stat(string, &st)) { + Perror(space, "invalid filename '%s'", string); + continue; + } + if (S_ISDIR(st.st_mode)) { + struct dirent **list; + int i, num; + num = scandir(string, &list, conf_name_filter, + alphasort); + if (num < 0) { + Perror(space, "invalid directory '%s'", string); + continue; + } + count = strlen(string); + for (i = 0; i < num; i++) { + string[count] = '\0'; + strlcat(string, "/", sizeof(string)); + strlcat(string, list[i]->d_name, sizeof(string)); + space->go_to = NULL; + space->rootdir = new_root_dir(string); + free(list[i]); + if (space->rootdir) { + err = parse(space, string); + free(space->rootdir); + } else + err = -ENOMEM; + if (space->go_to) { + Perror(space, "unterminated GOTO '%s'", space->go_to); + free(space->go_to); + } + if (err) + break; + } + free(list); + } else { + space->go_to = NULL; + space->rootdir = new_root_dir(string); + if (space->rootdir) { + err = parse(space, string); + free(space->rootdir); + } else + err = -ENOMEM; + if (space->go_to) { + Perror(space, "unterminated GOTO '%s'", space->go_to); + free(space->go_to); + } + } + space->go_to = go_to; + space->rootdir = rootdir; + space->filename = filename; + space->linenum = linenum; + if (space->quit) + break; + if (err) + break; + continue; + } + if (strncasecmp(key, "ACCESS", 6) == 0) { + if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + if (value[0] == '$') { + strlcpy(string, value, sizeof(string)); + apply_format(space, string, sizeof(string)); + if (string[0] == '/') + goto __access1; + } + if (value[0] != '/') { + strlcpy(string, space->rootdir, sizeof(string)); + strlcat(string, "/", sizeof(string)); + strlcat(string, value, sizeof(string)); + } else { + strlcpy(string, value, sizeof(string)); + } + apply_format(space, string, sizeof(string)); + __access1: + count = access(string, F_OK); + dbg("access(%s) = %i (%s)", string, count, value); + if (op == KEY_OP_MATCH && count != 0) + break; + if (op == KEY_OP_NOMATCH && count == 0) + break; + } else { + Perror(space, "invalid ACCESS operation"); + goto invalid; + } + continue; + } + if (strncasecmp(key, "PRINT", 5) == 0) { + if (op != KEY_OP_ASSIGN) { + Perror(space, "invalid PRINT operation"); + goto invalid; + } + strlcpy(string, value, sizeof(string)); + apply_format(space, string, sizeof(string)); + fwrite(string, strlen(string), 1, stdout); + continue; + } + if (strncasecmp(key, "ERROR", 5) == 0) { + if (op != KEY_OP_ASSIGN) { + Perror(space, "invalid ERROR operation"); + goto invalid; + } + strlcpy(string, value, sizeof(string)); + apply_format(space, string, sizeof(string)); + fwrite(string, strlen(string), 1, stderr); + continue; + } + if (strncasecmp(key, "EXIT", 4) == 0) { + if (op != KEY_OP_ASSIGN) { + Perror(space, "invalid EXIT operation"); + goto invalid; + } + strlcpy(string, value, sizeof(string)); + apply_format(space, string, sizeof(string)); + if (strcmp(string, "return") == 0) + return -EJUSTRETURN; + space->exit_code = strtol(string, NULL, 0); + space->quit = 1; + break; + } + if (strncasecmp(key, "CONFIG{", 7) == 0) { + attr = get_key_attribute(space, key + 6, string, sizeof(string)); + if (attr == NULL) { + Perror(space, "error parsing CONFIG attribute"); + goto invalid; + } + strlcpy(result, value, sizeof(result)); + apply_format(space, result, sizeof(result)); + if (op == KEY_OP_ASSIGN) { + err = value_set(space, attr, result); + dbg("CONFIG{%s}='%s'", attr, result); + break; + } else if (op == KEY_OP_MATCH || op == KEY_OP_NOMATCH) { + pair = value_find(space, attr); + if (pair == NULL) + break; + if (!do_match(key, op, result, pair->value)) + break; + } else { + Perror(space, "invalid CONFIG{} operation"); + goto invalid; + } + } + + Perror(space, "unknown key '%s'", key); + } + return err; + +invalid: + Perror(space, "invalid rule"); + return -EINVAL; +} + +static int parse(struct space *space, const char *filename) +{ + char *buf, *bufline, *line; + size_t bufsize, pos, count, linesize; + unsigned int linenum, i, j, linenum_adj; + int err; + + dbg("start of file '%s'", filename); + + if (file_map(filename, &buf, &bufsize) != 0) { + err = errno; + error("Unable to open file '%s': %s", filename, strerror(err)); + return -err; + } + + err = 0; + pos = 0; + linenum = 0; + linesize = 128; + line = malloc(linesize); + if (line == NULL) { + file_unmap(buf, bufsize); + return -ENOMEM; + } + space->filename = filename; + while (!err && pos < bufsize && !space->quit) { + count = line_width(buf, bufsize, pos); + bufline = buf + pos; + pos += count + 1; + linenum++; + + /* skip whitespaces */ + while (count > 0 && isspace(bufline[0])) { + bufline++; + count--; + } + if (count == 0) + continue; + + /* comment check */ + if (bufline[0] == '#') + continue; + + if (count > linesize - 1) { + free(line); + line = NULL; + linesize = (count + 127 + 1) & ~127; + if (linesize > 2048) { + error("file %s, line %i too long", filename, linenum); + err = -EINVAL; + break; + } + line = malloc(linesize); + if (line == NULL) { + err = -EINVAL; + break; + } + } + + /* skip backslash and newline from multiline rules */ + linenum_adj = 0; + for (i = j = 0; i < count; i++) { + if (bufline[i] == '\\' && bufline[i+1] == '\n') { + linenum_adj++; + continue; + } + line[j++] = bufline[i]; + } + line[j] = '\0'; + + dbg("read (%i) '%s'", linenum, line); + space->linenum = linenum; + err = parse_line(space, line, linesize); + if (err == -EJUSTRETURN) { + err = 0; + break; + } + linenum += linenum_adj; + } + + free(line); + space->filename = NULL; + space->linenum = -1; + file_unmap(buf, bufsize); + dbg("end of file '%s'", filename); + return err ? err : -abs(space->exit_code); +} + +int init(const char *cfgdir, const char *filename, int flags, const char *cardname) +{ + struct space *space; + struct snd_card_iterator iter; + int err = 0, lasterr = 0; + + sysfs_init(); + err = snd_card_iterator_sinit(&iter, cardname); + if (err < 0) + goto out; + while (snd_card_iterator_next(&iter)) { + err = snd_card_clean_cfgdir(cfgdir, iter.card); + if (err < 0) { + if (lasterr == 0) + lasterr = err; + continue; + } + err = init_ucm(flags, iter.card); + if (err == 0) + continue; + err = init_space(&space, iter.card); + if (err != 0) + continue; + space->rootdir = new_root_dir(filename); + if (space->rootdir != NULL) { + err = parse(space, filename); + if (!cardname && err <= -99) { /* non-fatal errors */ + if (lasterr == 0) + lasterr = err; + err = 0; + } + } + free_space(space); + if (err < 0) + goto out; + } + err = lasterr ? lasterr : snd_card_iterator_error(&iter); +out: + sysfs_cleanup(); + return err; +} diff --git a/alsactl/init_sysdeps.c b/alsactl/init_sysdeps.c new file mode 100644 index 0000000..f09b1ae --- /dev/null +++ b/alsactl/init_sysdeps.c @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2003 Greg Kroah-Hartman + * Copyright (C) 2005-2006 Kay Sievers + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#if defined(__GLIBC__) && !(defined(__UCLIBC__) && defined(__USE_BSD)) +#if !(__GLIBC_PREREQ(2, 38)) +static size_t strlcpy(char *dst, const char *src, size_t size) +{ + size_t bytes = 0; + char *q = dst; + const char *p = src; + char ch; + + while ((ch = *p++)) { + if (bytes+1 < size) + *q++ = ch; + bytes++; + } + + /* If size == 0 there is no space for a final null... */ + if (size) + *q = '\0'; + return bytes; +} + +static size_t strlcat(char *dst, const char *src, size_t size) +{ + size_t bytes = 0; + char *q = dst; + const char *p = src; + char ch; + + while (bytes < size && *q) { + q++; + bytes++; + } + if (bytes == size) + return (bytes + strlen(src)); + + while ((ch = *p++)) { + if (bytes+1 < size) + *q++ = ch; + bytes++; + } + + *q = '\0'; + return bytes; +} +#endif /* !(__GLIBC_PREREQ(2, 38)) */ +#endif /* __GLIBC__ */ diff --git a/alsactl/init_sysfs.c b/alsactl/init_sysfs.c new file mode 100644 index 0000000..57ef615 --- /dev/null +++ b/alsactl/init_sysfs.c @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2005-2006 Kay Sievers + * 2008 Jaroslav Kysela + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + + +static char sysfs_path[PATH_SIZE]; + +/* attribute value cache */ +static LIST_HEAD(attr_list); +struct sysfs_attr { + struct list_head node; + char path[PATH_SIZE]; + char *value; /* points to value_local if value is cached */ + char value_local[NAME_SIZE]; +}; + +static int sysfs_init(void) +{ + const char *env; + char sysfs_test[PATH_SIZE]; + + INIT_LIST_HEAD(&attr_list); + + env = getenv("SYSFS_PATH"); + if (env) { + strlcpy(sysfs_path, env, sizeof(sysfs_path)); + remove_trailing_chars(sysfs_path, '/'); + } else + strlcpy(sysfs_path, "/sys", sizeof(sysfs_path)); + dbg("sysfs_path='%s'", sysfs_path); + + strlcpy(sysfs_test, sysfs_path, sizeof(sysfs_test)); + strlcat(sysfs_test, "/kernel/uevent_seqnum", sizeof(sysfs_test)); + if (access(sysfs_test, F_OK)) { + strlcpy(sysfs_test, sysfs_path, sizeof(sysfs_test)); + strlcat(sysfs_test, "/kernel/uevent_helper", sizeof(sysfs_test)); + if (access(sysfs_test, F_OK)) { + error("sysfs path '%s' is invalid", sysfs_path); + return -errno; + } + } + + return 0; +} + +static void sysfs_cleanup(void) +{ + struct sysfs_attr *attr_loop; + struct sysfs_attr *attr_temp; + + list_for_each_entry_safe(attr_loop, attr_temp, &attr_list, node) { + list_del(&attr_loop->node); + free(attr_loop); + } +} + +static char *sysfs_attr_get_value(const char *devpath, const char *attr_name) +{ + char path_full[PATH_SIZE]; + const char *path; + char value[NAME_SIZE]; + struct sysfs_attr *attr_loop; + struct sysfs_attr *attr; + struct stat statbuf; + int fd; + ssize_t size; + size_t sysfs_len; + + dbg("open '%s'/'%s'", devpath, attr_name); + sysfs_len = strlcpy(path_full, sysfs_path, sizeof(path_full)); + path = &path_full[sysfs_len]; + strlcat(path_full, devpath, sizeof(path_full)); + strlcat(path_full, "/", sizeof(path_full)); + strlcat(path_full, attr_name, sizeof(path_full)); + + /* look for attribute in cache */ + list_for_each_entry(attr_loop, &attr_list, node) { + if (strcmp(attr_loop->path, path) == 0) { + dbg("found in cache '%s'", attr_loop->path); + return attr_loop->value; + } + } + + /* store attribute in cache (also negatives are kept in cache) */ + dbg("new uncached attribute '%s'", path_full); + attr = malloc(sizeof(struct sysfs_attr)); + if (attr == NULL) + return NULL; + memset(attr, 0x00, sizeof(struct sysfs_attr)); + strlcpy(attr->path, path, sizeof(attr->path)); + dbg("add to cache '%s'", path_full); + list_add(&attr->node, &attr_list); + + if (lstat(path_full, &statbuf) != 0) { + dbg("stat '%s' failed: %s", path_full, strerror(errno)); + goto out; + } + + if (S_ISLNK(statbuf.st_mode)) { + /* links return the last element of the target path */ + char link_target[PATH_SIZE + 1]; + int len; + const char *pos; + + len = readlink(path_full, link_target, sizeof(link_target) - 1); + if (len > 0) { + link_target[len] = '\0'; + pos = strrchr(link_target, '/'); + if (pos != NULL) { + dbg("cache '%s' with link value '%s'", path_full, pos+1); + strlcpy(attr->value_local, &pos[1], sizeof(attr->value_local)); + attr->value = attr->value_local; + } + } + goto out; + } + + /* skip directories */ + if (S_ISDIR(statbuf.st_mode)) + goto out; + + /* skip non-readable files */ + if ((statbuf.st_mode & S_IRUSR) == 0) + goto out; + + /* read attribute value */ + fd = open(path_full, O_RDONLY); + if (fd < 0) { + dbg("attribute '%s' does not exist", path_full); + goto out; + } + size = read(fd, value, sizeof(value)); + close(fd); + if (size < 0) + goto out; + if (size == sizeof(value)) + goto out; + + /* got a valid value, store and return it */ + value[size] = '\0'; + remove_trailing_chars(value, '\n'); + dbg("cache '%s' with attribute value '%s'", path_full, value); + strlcpy(attr->value_local, value, sizeof(attr->value_local)); + attr->value = attr->value_local; + +out: + return attr->value; +} diff --git a/alsactl/init_ucm.c b/alsactl/init_ucm.c new file mode 100644 index 0000000..31e8daf --- /dev/null +++ b/alsactl/init_ucm.c @@ -0,0 +1,80 @@ +/* + * Advanced Linux Sound Architecture Control Program - UCM Initialization + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include "alsactl.h" + +#ifdef HAVE_ALSA_USE_CASE_H + +#include + +/* + * Keep it as simple as possible. Execute commands from the + * FixedBootSequence and BootSequence only. + */ +int init_ucm(int flags, int cardno) +{ + snd_use_case_mgr_t *uc_mgr; + char id[32], *nodev; + int err; + + if (flags & FLAG_UCM_DISABLED) { + dbg("ucm disabled"); + return -ENXIO; + } + + nodev = (flags & FLAG_UCM_NODEV) ? "" : "-"; + snprintf(id, sizeof(id), "%shw:%d", nodev, cardno); + err = snd_use_case_mgr_open(&uc_mgr, id); + dbg("ucm open '%s': %d", id, err); + if (err < 0) + return err; + if (flags & FLAG_UCM_FBOOT) { + err = snd_use_case_set(uc_mgr, "_fboot", NULL); + dbg("ucm _fboot: %d", err); + if (err == -ENOENT && (flags & FLAG_UCM_BOOT) != 0) { + /* nothing */ + } else if (err < 0) { + goto _error; + } + } + if (flags & FLAG_UCM_BOOT) { + err = snd_use_case_set(uc_mgr, "_boot", NULL); + dbg("ucm _boot: %d", err); + if (err < 0) + goto _error; + if ((flags & FLAG_UCM_DEFAULTS) != 0) + err = snd_use_case_set(uc_mgr, "_defaults", NULL); + } +_error: + snd_use_case_mgr_close(uc_mgr); + return err; +} + +#else + +int init_ucm(int flags, int cardno) +{ + return -ENXIO; +} + +#endif diff --git a/alsactl/init_utils_run.c b/alsactl/init_utils_run.c new file mode 100644 index 0000000..dde490b --- /dev/null +++ b/alsactl/init_utils_run.c @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2004-2005 Kay Sievers + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#define MY_MAX(a,b) ((a) > (b) ? (a) : (b)) + +#define READ_END 0 +#define WRITE_END 1 + +static +int run_program1(struct space *space, + const char *command0, char *result, + size_t ressize, size_t *reslen, int log); + +static +int run_program0(struct space *space, + const char *command0, char *result, + size_t ressize, size_t *reslen, int log) +{ + int retval = 0; + int status; + int outpipe[2] = {-1, -1}; + int errpipe[2] = {-1, -1}; + pid_t pid; + char arg[PATH_SIZE]; + char program[PATH_SIZE]; + char *argv[(sizeof(arg) / 2) + 1]; + int devnull; + int i; + + /* build argv from comand */ + strlcpy(arg, command0, sizeof(arg)); + i = 0; + if (strchr(arg, ' ') != NULL) { + char *pos = arg; + + while (pos != NULL) { + if (pos[0] == '\'') { + /* don't separate if in apostrophes */ + pos++; + argv[i] = strsep(&pos, "\'"); + while (pos != NULL && pos[0] == ' ') + pos++; + } else { + argv[i] = strsep(&pos, " "); + } + dbg("arg[%i] '%s'", i, argv[i]); + i++; + } + argv[i] = NULL; + } else { + argv[0] = arg; + argv[1] = NULL; + } + info("'%s'", command0); + + /* prepare pipes from child to parent */ + if (result || log) { + if (pipe(outpipe) != 0) { + Perror(space, "pipe failed: %s", strerror(errno)); + return -1; + } + } + if (log) { + if (pipe(errpipe) != 0) { + Perror(space, "pipe failed: %s", strerror(errno)); + return -1; + } + } + + /* allow programs in /lib/alsa called without the path */ + if (strchr(argv[0], '/') == NULL) { + strlcpy(program, "/lib/alsa/", sizeof(program)); + strlcat(program, argv[0], sizeof(program)); + argv[0] = program; + } + + pid = fork(); + switch(pid) { + case 0: + /* child closes parent ends of pipes */ + if (outpipe[READ_END] > 0) + close(outpipe[READ_END]); + if (errpipe[READ_END] > 0) + close(errpipe[READ_END]); + + /* discard child output or connect to pipe */ + devnull = open("/dev/null", O_RDWR); + if (devnull > 0) { + dup2(devnull, STDIN_FILENO); + if (outpipe[WRITE_END] < 0) + dup2(devnull, STDOUT_FILENO); + if (errpipe[WRITE_END] < 0) + dup2(devnull, STDERR_FILENO); + close(devnull); + } else + Perror(space, "open /dev/null failed: %s", strerror(errno)); + if (outpipe[WRITE_END] > 0) { + dup2(outpipe[WRITE_END], STDOUT_FILENO); + close(outpipe[WRITE_END]); + } + if (errpipe[WRITE_END] > 0) { + dup2(errpipe[WRITE_END], STDERR_FILENO); + close(errpipe[WRITE_END]); + } + execv(argv[0], argv); + + /* we should never reach this */ + Perror(space, "exec of program '%s' failed", argv[0]); + _exit(1); + case -1: + Perror(space, "fork of '%s' failed: %s", argv[0], strerror(errno)); + return -1; + default: + /* read from child if requested */ + if (outpipe[READ_END] > 0 || errpipe[READ_END] > 0) { + ssize_t count; + size_t respos = 0; + + /* parent closes child ends of pipes */ + if (outpipe[WRITE_END] > 0) + close(outpipe[WRITE_END]); + if (errpipe[WRITE_END] > 0) + close(errpipe[WRITE_END]); + + /* read child output */ + while (outpipe[READ_END] > 0 || errpipe[READ_END] > 0) { + int fdcount; + fd_set readfds; + + FD_ZERO(&readfds); + if (outpipe[READ_END] > 0) + FD_SET(outpipe[READ_END], &readfds); + if (errpipe[READ_END] > 0) + FD_SET(errpipe[READ_END], &readfds); + fdcount = select(MY_MAX(outpipe[READ_END], errpipe[READ_END])+1, &readfds, NULL, NULL, NULL); + if (fdcount < 0) { + if (errno == EINTR) + continue; + retval = -1; + break; + } + + /* get stdout */ + if (outpipe[READ_END] > 0 && FD_ISSET(outpipe[READ_END], &readfds)) { + char inbuf[1024]; + char *pos; + char *line; + + count = read(outpipe[READ_END], inbuf, sizeof(inbuf)-1); + if (count <= 0) { + close(outpipe[READ_END]); + outpipe[READ_END] = -1; + if (count < 0) { + Perror(space, "stdin read failed: %s", strerror(errno)); + retval = -1; + } + continue; + } + inbuf[count] = '\0'; + + /* store result for rule processing */ + if (result) { + if (respos + count < ressize) { + memcpy(&result[respos], inbuf, count); + respos += count; + } else { + Perror(space, "ressize %ld too short", (long)ressize); + retval = -1; + } + } + pos = inbuf; + while ((line = strsep(&pos, "\n"))) + if (pos || line[0] != '\0') + info("'%s' (stdout) '%s'", argv[0], line); + } + + /* get stderr */ + if (errpipe[READ_END] > 0 && FD_ISSET(errpipe[READ_END], &readfds)) { + char errbuf[1024]; + char *pos; + char *line; + + count = read(errpipe[READ_END], errbuf, sizeof(errbuf)-1); + if (count <= 0) { + close(errpipe[READ_END]); + errpipe[READ_END] = -1; + if (count < 0) + Perror(space, "stderr read failed: %s", strerror(errno)); + continue; + } + errbuf[count] = '\0'; + pos = errbuf; + while ((line = strsep(&pos, "\n"))) + if (pos || line[0] != '\0') + info("'%s' (stderr) '%s'", argv[0], line); + } + } + if (outpipe[READ_END] > 0) + close(outpipe[READ_END]); + if (errpipe[READ_END] > 0) + close(errpipe[READ_END]); + + /* return the childs stdout string */ + if (result) { + result[respos] = '\0'; + dbg("result='%s'", result); + if (reslen) + *reslen = respos; + } + } + waitpid(pid, &status, 0); + if (WIFEXITED(status)) { + info("'%s' returned with status %i", argv[0], WEXITSTATUS(status)); + if (WEXITSTATUS(status) != 0) + retval = -1; + } else { + Perror(space, "'%s' abnormal exit", argv[0]); + retval = -1; + } + } + + return retval; +} + +static +int run_program(struct space *space, const char *command0, char *result, + size_t ressize, size_t *reslen, int log) +{ + if (command0[0] == '_' && command0[1] == '_') + return run_program1(space, command0, result, ressize, reslen, log); + return run_program0(space, command0, result, ressize, reslen, log); +} diff --git a/alsactl/init_utils_string.c b/alsactl/init_utils_string.c new file mode 100644 index 0000000..01ea800 --- /dev/null +++ b/alsactl/init_utils_string.c @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2004-2005 Kay Sievers + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + + +static void remove_trailing_chars(char *path, char c) +{ + size_t len; + + len = strlen(path); + while (len > 0 && path[len-1] == c) + path[--len] = '\0'; +} + +/* count of characters used to encode one unicode char */ +static int utf8_encoded_expected_len(const char *str) +{ + unsigned char c = (unsigned char)str[0]; + + if (c < 0x80) + return 1; + if ((c & 0xe0) == 0xc0) + return 2; + if ((c & 0xf0) == 0xe0) + return 3; + if ((c & 0xf8) == 0xf0) + return 4; + if ((c & 0xfc) == 0xf8) + return 5; + if ((c & 0xfe) == 0xfc) + return 6; + return 0; +} + +/* decode one unicode char */ +static int utf8_encoded_to_unichar(const char *str) +{ + int unichar; + int len; + int i; + + len = utf8_encoded_expected_len(str); + switch (len) { + case 1: + return (int)str[0]; + case 2: + unichar = str[0] & 0x1f; + break; + case 3: + unichar = (int)str[0] & 0x0f; + break; + case 4: + unichar = (int)str[0] & 0x07; + break; + case 5: + unichar = (int)str[0] & 0x03; + break; + case 6: + unichar = (int)str[0] & 0x01; + break; + default: + return -1; + } + + for (i = 1; i < len; i++) { + if (((int)str[i] & 0xc0) != 0x80) + return -1; + unichar <<= 6; + unichar |= (int)str[i] & 0x3f; + } + + return unichar; +} + +/* expected size used to encode one unicode char */ +static int utf8_unichar_to_encoded_len(int unichar) +{ + if (unichar < 0x80) + return 1; + if (unichar < 0x800) + return 2; + if (unichar < 0x10000) + return 3; + if (unichar < 0x200000) + return 4; + if (unichar < 0x4000000) + return 5; + return 6; +} + +/* check if unicode char has a valid numeric range */ +static int utf8_unichar_valid_range(int unichar) +{ + if (unichar > 0x10ffff) + return 0; + if ((unichar & 0xfffff800) == 0xd800) + return 0; + if ((unichar > 0xfdcf) && (unichar < 0xfdf0)) + return 0; + if ((unichar & 0xffff) == 0xffff) + return 0; + return 1; +} + +/* validate one encoded unicode char and return its length */ +static int utf8_encoded_valid_unichar(const char *str) +{ + int len; + int unichar; + int i; + + len = utf8_encoded_expected_len(str); + if (len == 0) + return -1; + + /* ascii is valid */ + if (len == 1) + return 1; + + /* check if expected encoded chars are available */ + for (i = 0; i < len; i++) + if ((str[i] & 0x80) != 0x80) + return -1; + + unichar = utf8_encoded_to_unichar(str); + + /* check if encoded length matches encoded value */ + if (utf8_unichar_to_encoded_len(unichar) != len) + return -1; + + /* check if value has valid range */ + if (!utf8_unichar_valid_range(unichar)) + return -1; + + return len; +} + +/* replace everything but whitelisted plain ascii and valid utf8 */ +static int replace_untrusted_chars(char *str) +{ + size_t i = 0; + int replaced = 0; + + while (str[i] != '\0') { + int len; + + /* valid printable ascii char */ + if ((str[i] >= '0' && str[i] <= '9') || + (str[i] >= 'A' && str[i] <= 'Z') || + (str[i] >= 'a' && str[i] <= 'z') || + strchr(" #$%+-./:=?@_,", str[i])) { + i++; + continue; + } + /* valid utf8 is accepted */ + len = utf8_encoded_valid_unichar(&str[i]); + if (len > 1) { + i += len; + continue; + } + + /* everything else is garbage */ + str[i] = '_'; + i++; + replaced++; + } + + return replaced; +} diff --git a/alsactl/list.h b/alsactl/list.h new file mode 100644 index 0000000..8626630 --- /dev/null +++ b/alsactl/list.h @@ -0,0 +1,289 @@ +/* + * Copied from the Linux kernel source tree, version 2.6.0-test1. + * + * Licensed under the GPL v2 as per the whole kernel source tree. + * + */ + +#ifndef _LIST_H +#define _LIST_H + +/** + * container_of - cast a member of a structure out to the containing structure + * + * @ptr: the pointer to the member. + * @type: the type of the container struct this is embedded in. + * @member: the name of the member within the struct. + * + */ +#define container_of(ptr, type, member) ({ \ + const typeof( ((type *)0)->member ) *__mptr = (ptr); \ + (type *)( (char *)__mptr - offsetof(type,member) );}) + +/* + * These are non-NULL pointers that will result in page faults + * under normal circumstances, used to verify that nobody uses + * non-initialized list entries. + */ +#define LIST_POISON1 ((void *) 0x00100100) +#define LIST_POISON2 ((void *) 0x00200200) + +/* + * Simple doubly linked list implementation. + * + * Some of the internal functions ("__xxx") are useful when + * manipulating whole lists rather than single entries, as + * sometimes we already know the next/prev entries and we can + * generate better code by using them directly rather than + * using the generic single-entry routines. + */ + +struct list_head { + struct list_head *next, *prev; +}; + +#define LIST_HEAD_INIT(name) { &(name), &(name) } + +#define LIST_HEAD(name) \ + struct list_head name = LIST_HEAD_INIT(name) + +#define INIT_LIST_HEAD(ptr) do { \ + (ptr)->next = (ptr); (ptr)->prev = (ptr); \ +} while (0) + +/* + * Insert a new entry between two known consecutive entries. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_add(struct list_head *new, + struct list_head *prev, + struct list_head *next) +{ + next->prev = new; + new->next = next; + new->prev = prev; + prev->next = new; +} + +/** + * list_add - add a new entry + * @new: new entry to be added + * @head: list head to add it after + * + * Insert a new entry after the specified head. + * This is good for implementing stacks. + */ +static inline void list_add(struct list_head *new, struct list_head *head) +{ + __list_add(new, head, head->next); +} + +/** + * list_add_tail - add a new entry + * @new: new entry to be added + * @head: list head to add it before + * + * Insert a new entry before the specified head. + * This is useful for implementing queues. + */ +static inline void list_add_tail(struct list_head *new, struct list_head *head) +{ + __list_add(new, head->prev, head); +} + +/* + * Delete a list entry by making the prev/next entries + * point to each other. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_del(struct list_head * prev, struct list_head * next) +{ + next->prev = prev; + prev->next = next; +} + +/** + * list_del - deletes entry from list. + * @entry: the element to delete from the list. + * Note: list_empty on entry does not return true after this, the entry is + * in an undefined state. + */ +static inline void list_del(struct list_head *entry) +{ + __list_del(entry->prev, entry->next); + entry->next = LIST_POISON1; + entry->prev = LIST_POISON2; +} + +/** + * list_del_init - deletes entry from list and reinitialize it. + * @entry: the element to delete from the list. + */ +static inline void list_del_init(struct list_head *entry) +{ + __list_del(entry->prev, entry->next); + INIT_LIST_HEAD(entry); +} + +/** + * list_move - delete from one list and add as another's head + * @list: the entry to move + * @head: the head that will precede our entry + */ +static inline void list_move(struct list_head *list, struct list_head *head) +{ + __list_del(list->prev, list->next); + list_add(list, head); +} + +/** + * list_move_tail - delete from one list and add as another's tail + * @list: the entry to move + * @head: the head that will follow our entry + */ +static inline void list_move_tail(struct list_head *list, + struct list_head *head) +{ + __list_del(list->prev, list->next); + list_add_tail(list, head); +} + +/** + * list_empty - tests whether a list is empty + * @head: the list to test. + */ +static inline int list_empty(struct list_head *head) +{ + return head->next == head; +} + +static inline void __list_splice(struct list_head *list, + struct list_head *head) +{ + struct list_head *first = list->next; + struct list_head *last = list->prev; + struct list_head *at = head->next; + + first->prev = head; + head->next = first; + + last->next = at; + at->prev = last; +} + +/** + * list_splice - join two lists + * @list: the new list to add. + * @head: the place to add it in the first list. + */ +static inline void list_splice(struct list_head *list, struct list_head *head) +{ + if (!list_empty(list)) + __list_splice(list, head); +} + +/** + * list_splice_init - join two lists and reinitialise the emptied list. + * @list: the new list to add. + * @head: the place to add it in the first list. + * + * The list at @list is reinitialised + */ +static inline void list_splice_init(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) { + __list_splice(list, head); + INIT_LIST_HEAD(list); + } +} + +/** + * list_entry - get the struct for this entry + * @ptr: the &struct list_head pointer. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_struct within the struct. + */ +#define list_entry(ptr, type, member) \ + container_of(ptr, type, member) + +/** + * list_for_each - iterate over a list + * @pos: the &struct list_head to use as a loop counter. + * @head: the head for your list. + */ +#define list_for_each(pos, head) \ + for (pos = (head)->next; pos != (head); \ + pos = pos->next) + +/** + * __list_for_each - iterate over a list + * @pos: the &struct list_head to use as a loop counter. + * @head: the head for your list. + * + * This variant differs from list_for_each() in that it's the + * simplest possible list iteration code. + * Use this for code that knows the list to be very short (empty + * or 1 entry) most of the time. + */ +#define __list_for_each(pos, head) \ + for (pos = (head)->next; pos != (head); pos = pos->next) + +/** + * list_for_each_prev - iterate over a list backwards + * @pos: the &struct list_head to use as a loop counter. + * @head: the head for your list. + */ +#define list_for_each_prev(pos, head) \ + for (pos = (head)->prev; pos != (head); pos = pos->prev) + +/** + * list_for_each_safe - iterate over a list safe against removal of list entry + * @pos: the &struct list_head to use as a loop counter. + * @n: another &struct list_head to use as temporary storage + * @head: the head for your list. + */ +#define list_for_each_safe(pos, n, head) \ + for (pos = (head)->next, n = pos->next; pos != (head); \ + pos = n, n = pos->next) + +/** + * list_for_each_entry - iterate over list of given type + * @pos: the type * to use as a loop counter. + * @head: the head for your list. + * @member: the name of the list_struct within the struct. + */ +#define list_for_each_entry(pos, head, member) \ + for (pos = list_entry((head)->next, typeof(*pos), member); \ + &pos->member != (head); \ + pos = list_entry(pos->member.next, typeof(*pos), member)) + +/** + * list_for_each_entry_reverse - iterate backwards over list of given type. + * @pos: the type * to use as a loop counter. + * @head: the head for your list. + * @member: the name of the list_struct within the struct. + */ +#define list_for_each_entry_reverse(pos, head, member) \ + for (pos = list_entry((head)->prev, typeof(*pos), member); \ + &pos->member != (head); \ + pos = list_entry(pos->member.prev, typeof(*pos), member)) + +/** + * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry + * @pos: the type * to use as a loop counter. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_struct within the struct. + */ +#define list_for_each_entry_safe(pos, n, head, member) \ + for (pos = list_entry((head)->next, typeof(*pos), member), \ + n = list_entry(pos->member.next, typeof(*pos), member); \ + &pos->member != (head); \ + pos = n, n = list_entry(n->member.next, typeof(*n), member)) + +#endif /* _LIST_H */ diff --git a/alsactl/lock.c b/alsactl/lock.c new file mode 100644 index 0000000..4927b70 --- /dev/null +++ b/alsactl/lock.c @@ -0,0 +1,211 @@ +/* + * Advanced Linux Sound Architecture Control Program + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "alsactl.h" +#include "os_compat.h" + +#define PATH_SIZE 512 + +static int alarm_flag; + +static void signal_handler_alarm(int sig ATTRIBUTE_UNUSED) +{ + alarm_flag = 1; +} + +static int state_lock_(const char *lock_file, int lock, int timeout, int _fd) +{ + int fd = -1, err = 0; + struct flock lck; + struct stat st; + char lcktxt[14]; + struct sigaction sig_alarm, sig_alarm_orig; + struct itimerval itv; + + if (do_lock <= 0) + return 0; + + lck.l_type = lock ? F_WRLCK : F_UNLCK; + lck.l_whence = SEEK_SET; + lck.l_start = 0; + lck.l_len = 11; + lck.l_pid = 0; + if (lock) { + snprintf(lcktxt, sizeof(lcktxt), "%10li\n", (long)getpid()); + } else { + snprintf(lcktxt, sizeof(lcktxt), "%10s\n", ""); + fd = _fd; + } + while (fd < 0 && timeout-- > 0) { + fd = open(lock_file, O_RDWR); + if (!lock && fd < 0) { + err = -EIO; + goto out; + } + if (fd < 0) { + fd = open(lock_file, O_RDWR|O_CREAT|O_EXCL, 0644); + if (fd < 0) { + if (errno == EBUSY || errno == EAGAIN) { + sleep(1); + continue; + } + if (errno == EEXIST) { + fd = open(lock_file, O_RDWR); + if (fd >= 0) + break; + } + err = -errno; + goto out; + } + } + } + if (fd < 0 && timeout <= 0) { + err = -EBUSY; + goto out; + } + if (fstat(fd, &st) < 0) { + err = -errno; + goto out; + } + if (st.st_size != 11 || !lock) { + if (write(fd, lcktxt, 11) != 11) { + err = -EIO; + goto out; + } + if (lock && lseek(fd, 0, SEEK_SET)) { + err = -errno; + goto out; + } + } + alarm_flag = 0; + memset(&sig_alarm, 0, sizeof(sig_alarm)); + sigemptyset(&sig_alarm.sa_mask); + sig_alarm.sa_handler = signal_handler_alarm; + if (sigaction(SIGALRM, &sig_alarm, &sig_alarm_orig) < 0) { + err = -ENXIO; + goto out; + } + memset(&itv, 0, sizeof(itv)); + itv.it_value.tv_sec = timeout; + if (setitimer(ITIMER_REAL, &itv, NULL) < 0) { + err = -ENXIO; + sigaction(SIGALRM, &sig_alarm_orig, NULL); + goto out; + } + while (alarm_flag == 0) { + if (fcntl(fd, F_SETLKW, &lck) == 0) + break; + if (errno == EAGAIN || errno == ERESTART) + continue; + } + memset(&itv, 0, sizeof(itv)); + setitimer(ITIMER_REAL, &itv, NULL); + sigaction(SIGALRM, &sig_alarm_orig, NULL); + if (alarm_flag) { + err = -EBUSY; + goto out; + } + if (lock) { + if (write(fd, lcktxt, 11) != 11) { + err = -EIO; + goto out; + } + return fd; + } + err = 0; + +out: + if (fd >= 0) + close(fd); + return err; +} + +static void state_lock_file(char *buf, size_t buflen) +{ + if (lockfile[0] == '/') + snprintf(buf, buflen, "%s", lockfile); + else + snprintf(buf, buflen, "%s/%s", lockpath, lockfile); +} + +int state_lock(const char *file, int timeout) +{ + char fn[PATH_SIZE]; + int err; + + state_lock_file(fn, sizeof(fn)); + err = state_lock_(fn, 1, timeout, -1); + if (err < 0) + error("file %s lock error: %s", file, strerror(-err)); + return err; +} + +int state_unlock(int _fd, const char *file) +{ + char fn[PATH_SIZE]; + int err; + + state_lock_file(fn, sizeof(fn)); + err = state_lock_(fn, 0, 10, _fd); + if (err < 0) + error("file %s unlock error: %s", file, strerror(-err)); + return err; +} + +static void card_lock_file(char *buf, size_t buflen, int card_number) +{ + snprintf(buf, buflen, "%s/card%i.lock", lockpath, card_number); +} + +int card_lock(int card_number, int timeout) +{ + char fn[PATH_SIZE]; + int err; + + card_lock_file(fn, sizeof(fn), card_number); + err = state_lock_(fn, 1, timeout, -1); + if (err < 0) + error("card %d lock error: %s", card_number, strerror(-err)); + return err; +} + +int card_unlock(int _fd, int card_number) +{ + char fn[PATH_SIZE]; + int err; + + card_lock_file(fn, sizeof(fn), card_number); + err = state_lock_(fn, 0, 10, _fd); + if (err < 0) + error("card %d unlock error: %s", card_number, strerror(-err)); + return err; +} diff --git a/alsactl/monitor.c b/alsactl/monitor.c new file mode 100644 index 0000000..36e9398 --- /dev/null +++ b/alsactl/monitor.c @@ -0,0 +1,452 @@ +/* + * Advanced Linux Sound Architecture Control Program + * Copyright (c) by Takashi Iwai + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "list.h" + +#include "alsactl.h" + +struct src_entry { + snd_ctl_t *handle; + char *name; + unsigned int pfd_count; + struct list_head list; +}; + +static void remove_source_entry(struct src_entry *entry) +{ + list_del(&entry->list); + if (entry->handle) + snd_ctl_close(entry->handle); + free(entry->name); + free(entry); +} + +static void clear_source_list(struct list_head *srcs) +{ + struct src_entry *entry, *tmp; + + list_for_each_entry_safe(entry, tmp, srcs, list) + remove_source_entry(entry); +} + +static int insert_source_entry(struct list_head *srcs, snd_ctl_t *handle, + const char *name) +{ + struct src_entry *entry; + int count; + int err; + + entry = calloc(1, sizeof(*entry)); + if (!entry) + return -ENOMEM; + INIT_LIST_HEAD(&entry->list); + entry->handle = handle; + + entry->name = strdup(name); + if (!entry->name) { + err = -ENOMEM; + goto error; + } + + count = snd_ctl_poll_descriptors_count(handle); + if (count < 0) { + err = count; + goto error; + } + if (count == 0) { + err = -ENXIO; + goto error; + } + entry->pfd_count = count; + + list_add_tail(&entry->list, srcs); + + return 0; +error: + remove_source_entry(entry); + return err; +} + +static int open_ctl(const char *name, snd_ctl_t **ctlp) +{ + snd_ctl_t *ctl; + int err; + + err = snd_ctl_open(&ctl, name, SND_CTL_READONLY); + if (err < 0) { + fprintf(stderr, "Cannot open ctl %s\n", name); + return err; + } + err = snd_ctl_subscribe_events(ctl, 1); + if (err < 0) { + fprintf(stderr, "Cannot open subscribe events to ctl %s\n", name); + snd_ctl_close(ctl); + return err; + } + *ctlp = ctl; + return 0; +} + +static inline bool seek_entry_by_name(struct list_head *srcs, const char *name) +{ + struct src_entry *entry; + + list_for_each_entry(entry, srcs, list) { + if (!strcmp(entry->name, name)) + return true; + } + + return false; +} + +static int prepare_source_entry(struct list_head *srcs, const char *name) +{ + snd_ctl_t *handle; + int err; + + if (!name) { + struct snd_card_iterator iter; + const char *cardname; + + snd_card_iterator_init(&iter, -1); + while ((cardname = snd_card_iterator_next(&iter))) { + if (seek_entry_by_name(srcs, cardname)) + continue; + err = open_ctl(cardname, &handle); + if (err < 0) + return err; + err = insert_source_entry(srcs, handle, cardname); + if (err < 0) + return err; + } + } else { + if (seek_entry_by_name(srcs, name)) + return 0; + err = open_ctl(name, &handle); + if (err < 0) + return err; + err = insert_source_entry(srcs, handle, name); + if (err < 0) + return err; + } + + return 0; +} + +static int check_control_cdev(int infd, bool *retry) +{ + struct inotify_event *ev; + char *buf; + int err = 0; + + buf = calloc(1, sizeof(*ev) + NAME_MAX); + if (!buf) + return -ENOMEM; + + while (1) { + ssize_t len = read(infd, buf, sizeof(*ev) + NAME_MAX); + if (len < 0) { + if (errno != EAGAIN) + err = errno; + break; + } else if (len == 0) { + break; + } + + size_t pos = 0; + while (pos < (size_t)len) { + ev = (struct inotify_event *)(buf + pos); + if ((ev->mask & IN_CREATE) && + strstr(ev->name, "controlC") == ev->name) + *retry = true; + pos += sizeof(*ev) + ev->len; + } + } + + free(buf); + + return err; +} + +static int print_event(snd_ctl_t *ctl, const char *name) +{ + snd_ctl_event_t *event; + unsigned int mask; + int err; + + snd_ctl_event_alloca(&event); + err = snd_ctl_read(ctl, event); + if (err < 0) + return err; + + if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM) + return 0; + + printf("node %s, #%d (%i,%i,%i,%s,%i)", + name, + snd_ctl_event_elem_get_numid(event), + snd_ctl_event_elem_get_interface(event), + snd_ctl_event_elem_get_device(event), + snd_ctl_event_elem_get_subdevice(event), + snd_ctl_event_elem_get_name(event), + snd_ctl_event_elem_get_index(event)); + + mask = snd_ctl_event_elem_get_mask(event); + if (mask == SND_CTL_EVENT_MASK_REMOVE) { + printf(" REMOVE\n"); + return 0; + } + + if (mask & SND_CTL_EVENT_MASK_VALUE) + printf(" VALUE"); + if (mask & SND_CTL_EVENT_MASK_INFO) + printf(" INFO"); + if (mask & SND_CTL_EVENT_MASK_ADD) + printf(" ADD"); + if (mask & SND_CTL_EVENT_MASK_TLV) + printf(" TLV"); + printf("\n"); + fflush(stdout); + return 0; +} + +static int operate_dispatcher(int epfd, uint32_t op, struct epoll_event *epev, + struct src_entry *entry) +{ + struct pollfd *pfds; + int count; + int i; + int err = 0; + + pfds = calloc(entry->pfd_count, sizeof(*pfds)); + if (!pfds) + return -ENOMEM; + + count = snd_ctl_poll_descriptors(entry->handle, pfds, entry->pfd_count); + if (count < 0) { + err = count; + goto end; + } + if (count != (int)entry->pfd_count) { + err = -EIO; + goto end; + } + + for (i = 0; i < (int)entry->pfd_count; ++i) { + err = epoll_ctl(epfd, op, pfds[i].fd, epev); + if (err < 0) + break; + } +end: + free(pfds); + return err; +} + +static int prepare_dispatcher(int epfd, int sigfd, int infd, + struct list_head *srcs) +{ + struct epoll_event ev = {0}; + struct src_entry *entry; + int err = 0; + + ev.events = EPOLLIN; + ev.data.fd = sigfd; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, sigfd, &ev) < 0) + return -errno; + + ev.events = EPOLLIN; + ev.data.fd = infd; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, infd, &ev) < 0) + return -errno; + + list_for_each_entry(entry, srcs, list) { + ev.events = EPOLLIN; + ev.data.ptr = (void *)entry; + err = operate_dispatcher(epfd, EPOLL_CTL_ADD, &ev, entry); + if (err < 0) + break; + } + + return err; +} + +static int run_dispatcher(int epfd, int sigfd, int infd, struct list_head *srcs, + bool *retry) +{ + struct src_entry *entry; + unsigned int max_ev_count; + struct epoll_event *epev; + int err = 0; + + max_ev_count = 0; + list_for_each_entry(entry, srcs, list) + max_ev_count += entry->pfd_count; + + epev = calloc(max_ev_count, sizeof(*epev)); + if (!epev) + return -ENOMEM; + + while (true) { + int count; + int i; + + count = epoll_wait(epfd, epev, max_ev_count, -1); + if (count < 0) { + if (errno == EINTR) + continue; + err = count; + break; + } + if (count == 0) + continue; + + for (i = 0; i < count; ++i) { + struct epoll_event *ev = epev + i; + + if (ev->data.fd == sigfd) + goto end; + + if (ev->data.fd == infd) { + err = check_control_cdev(infd, retry); + if (err < 0 || *retry) + goto end; + continue; + } + + entry = ev->data.ptr; + if (ev->events & EPOLLIN) + print_event(entry->handle, entry->name); + if (ev->events & EPOLLERR) { + operate_dispatcher(epfd, EPOLL_CTL_DEL, NULL, entry); + remove_source_entry(entry); + } + } + } +end: + free(epev); + return err; +} + +static void clear_dispatcher(int epfd, int sigfd, int infd, + struct list_head *srcs) +{ + struct src_entry *entry; + + list_for_each_entry(entry, srcs, list) + operate_dispatcher(epfd, EPOLL_CTL_DEL, NULL, entry); + + epoll_ctl(epfd, EPOLL_CTL_DEL, infd, NULL); + + epoll_ctl(epfd, EPOLL_CTL_DEL, sigfd, NULL); +} + +static int prepare_signalfd(int *sigfd) +{ + sigset_t mask; + int fd; + + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + + if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) + return -errno; + + fd = signalfd(-1, &mask, 0); + if (fd < 0) + return -errno; + *sigfd = fd; + + return 0; +} + +int monitor(const char *name) +{ + LIST_HEAD(srcs); + int sigfd = 0; + int epfd; + int infd; + int wd = 0; + bool retry; + int err = 0; + + err = prepare_signalfd(&sigfd); + if (err < 0) + return err; + + epfd = epoll_create(1); + if (epfd < 0) { + close(sigfd); + return -errno; + } + + infd = inotify_init1(IN_NONBLOCK); + if (infd < 0) { + err = -errno; + goto error; + } + wd = inotify_add_watch(infd, "/dev/snd/", IN_CREATE); + if (wd < 0) { + err = -errno; + goto error; + } +retry: + retry = false; + err = prepare_source_entry(&srcs, name); + if (err < 0) + goto error; + + err = prepare_dispatcher(epfd, sigfd, infd, &srcs); + if (err >= 0) + err = run_dispatcher(epfd, sigfd, infd, &srcs, &retry); + clear_dispatcher(epfd, sigfd, infd, &srcs); + + if (retry) { + // A simple makeshift for timing gap between creation of nodes + // by devtmpfs and chmod() by udevd. + struct timespec req = { .tv_sec = 1 }; + nanosleep(&req, NULL); + goto retry; + } +error: + clear_source_list(&srcs); + + if (wd > 0) + inotify_rm_watch(infd, wd); + close(infd); + + close(epfd); + + close(sigfd); + + return err; +} diff --git a/alsactl/state.c b/alsactl/state.c new file mode 100644 index 0000000..3ccaab6 --- /dev/null +++ b/alsactl/state.c @@ -0,0 +1,1777 @@ +/* + * Advanced Linux Sound Architecture Control Program + * Copyright (c) by Abramo Bagnara + * Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include "alsactl.h" + + +static char *id_str(snd_ctl_elem_id_t *id) +{ + static char str[128]; + assert(id); + sprintf(str, "%i,%i,%i,%s,%i", + snd_ctl_elem_id_get_interface(id), + snd_ctl_elem_id_get_device(id), + snd_ctl_elem_id_get_subdevice(id), + snd_ctl_elem_id_get_name(id), + snd_ctl_elem_id_get_index(id)); + return str; +} + +static char *num_str(long n) +{ + static char str[32]; + sprintf(str, "%ld", n); + return str; +} + +static int snd_config_integer_add(snd_config_t *father, char *id, long integer) +{ + int err; + snd_config_t *leaf; + err = snd_config_imake_integer(&leaf, id, integer); + if (err < 0) + return err; + err = snd_config_add(father, leaf); + if (err < 0) { + snd_config_delete(leaf); + return err; + } + return 0; +} + +static int snd_config_integer64_add(snd_config_t *father, char *id, long long integer) +{ + int err; + snd_config_t *leaf; + err = snd_config_imake_integer64(&leaf, id, integer); + if (err < 0) + return err; + err = snd_config_add(father, leaf); + if (err < 0) { + snd_config_delete(leaf); + return err; + } + return 0; +} + +static int snd_config_string_add(snd_config_t *father, const char *id, const char *string) +{ + int err; + snd_config_t *leaf; + err = snd_config_imake_string(&leaf, id, string); + if (err < 0) + return err; + err = snd_config_add(father, leaf); + if (err < 0) { + snd_config_delete(leaf); + return err; + } + return 0; +} + +static int snd_config_compound_add(snd_config_t *father, const char *id, int join, + snd_config_t **node) +{ + int err; + snd_config_t *leaf; + err = snd_config_make_compound(&leaf, id, join); + if (err < 0) + return err; + err = snd_config_add(father, leaf); + if (err < 0) { + snd_config_delete(leaf); + return err; + } + *node = leaf; + return 0; +} + +#define MAX_USER_TLV_SIZE 64 + +static char *tlv_to_str(unsigned int *tlv) +{ + int i, len = tlv[1] / 4 + 2; + char *s, *p; + + if (len >= MAX_USER_TLV_SIZE) + return NULL; + s = malloc(len * 8 + 1); + if (! s) + return NULL; + p = s; + for (i = 0; i < len; i++) { + sprintf(p, "%08x", tlv[i]); + p += 8; + } + return s; +} + +static unsigned int *str_to_tlv(const char *s) +{ + int i, j, c, len; + unsigned int *tlv; + + len = strlen(s); + if (len % 8) /* aligned to 4 bytes (= 8 letters) */ + return NULL; + len /= 8; + if (len > MAX_USER_TLV_SIZE) + return NULL; + tlv = malloc(sizeof(int) * len); + if (! tlv) + return NULL; + for (i = 0; i < len; i++) { + tlv[i] = 0; + for (j = 0; j < 8; j++) { + if ((c = hextodigit(*s++)) < 0) { + free(tlv); + return NULL; + } + tlv[i] = (tlv[i] << 4) | c; + } + } + return tlv; +} + +/* + * add the TLV string, dB ranges, and dB values to comment fields + */ +static int add_tlv_comments(snd_ctl_t *handle, snd_ctl_elem_id_t *id, + snd_ctl_elem_info_t *info, snd_ctl_elem_value_t *ctl, + snd_config_t *comment) +{ + unsigned int tlv[MAX_USER_TLV_SIZE]; + unsigned int *db; + long rangemin, rangemax; + long dbmin, dbmax, dbgain; + snd_config_t *value; + unsigned int i, count; + int err; + + if (snd_ctl_elem_tlv_read(handle, id, tlv, sizeof(tlv)) < 0) + return 0; /* ignore error */ + + if (snd_ctl_elem_info_is_tlv_writable(info)) { + char *s = tlv_to_str(tlv); + if (s) { + err = snd_config_string_add(comment, "tlv", s); + free(s); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + } + } + + err = snd_tlv_parse_dB_info(tlv, sizeof(tlv), &db); + if (err <= 0) + return 0; + + rangemin = snd_ctl_elem_info_get_min(info); + rangemax = snd_ctl_elem_info_get_max(info); + snd_tlv_get_dB_range(db, rangemin, rangemax, &dbmin, &dbmax); + if (err < 0) + return err; + snd_config_integer_add(comment, "dbmin", dbmin); + snd_config_integer_add(comment, "dbmax", dbmax); + + if (snd_ctl_elem_info_get_type(info) == SND_CTL_ELEM_TYPE_INTEGER) { + err = snd_config_compound_add(comment, "dbvalue", 1, &value); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + return err; + } + count = snd_ctl_elem_info_get_count(info); + for (i = 0; i < count; i++) { + err = snd_tlv_convert_to_dB(db, rangemin, rangemax, + snd_ctl_elem_value_get_integer(ctl, i), &dbgain); + if (err < 0) { + error("snd_tlv_convert_to_dB: %s", snd_strerror(err)); + return err; + } + err = snd_config_integer_add(value, num_str(i), dbgain); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + } + } + return 0; +} + +static int get_control(snd_ctl_t *handle, snd_ctl_elem_id_t *id, snd_config_t *top) +{ + snd_ctl_elem_value_t *ctl; + snd_ctl_elem_info_t *info; + snd_config_t *control, *comment, *item = NULL, *value; + const char *s; + char buf[256]; + unsigned int idx; + int err; + unsigned int device, subdevice, index; + const char *name; + snd_ctl_elem_type_t type; + unsigned int count; + snd_ctl_elem_value_alloca(&ctl); + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_info_set_id(info, id); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + error("Cannot read control info '%s': %s", id_str(id), snd_strerror(err)); + return err; + } + + if (!snd_ctl_elem_info_is_readable(info)) + return 0; + snd_ctl_elem_value_set_id(ctl, id); + err = snd_ctl_elem_read(handle, ctl); + if (err < 0) { + error("Cannot read control '%s': %s", id_str(id), snd_strerror(err)); + return err; + } + + err = snd_config_compound_add(top, num_str(snd_ctl_elem_info_get_numid(info)), 0, &control); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + return err; + } + err = snd_config_make_compound(&comment, "comment", 0); + if (err < 0) { + error("snd_config_make_compound: %s", snd_strerror(err)); + return err; + } + + buf[0] = '\0'; + buf[1] = '\0'; + if (snd_ctl_elem_info_is_readable(info)) + strcat(buf, " read"); + if (snd_ctl_elem_info_is_writable(info)) + strcat(buf, " write"); + if (snd_ctl_elem_info_is_inactive(info)) + strcat(buf, " inactive"); + if (snd_ctl_elem_info_is_volatile(info)) + strcat(buf, " volatile"); + if (snd_ctl_elem_info_is_locked(info)) + strcat(buf, " locked"); + if (snd_ctl_elem_info_is_user(info)) + strcat(buf, " user"); + err = snd_config_string_add(comment, "access", buf + 1); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + + type = snd_ctl_elem_info_get_type(info); + device = snd_ctl_elem_info_get_device(info); + subdevice = snd_ctl_elem_info_get_subdevice(info); + index = snd_ctl_elem_info_get_index(info); + name = snd_ctl_elem_info_get_name(info); + count = snd_ctl_elem_info_get_count(info); + s = snd_ctl_elem_type_name(type); + err = snd_config_string_add(comment, "type", s); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + err = snd_config_integer_add(comment, "count", count); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + break; + case SND_CTL_ELEM_TYPE_INTEGER: + { + long min = snd_ctl_elem_info_get_min(info); + long max = snd_ctl_elem_info_get_max(info); + long step = snd_ctl_elem_info_get_step(info); + if (step) + sprintf(buf, "%li - %li (step %li)", min, max, step); + else + sprintf(buf, "%li - %li", min, max); + err = snd_config_string_add(comment, "range", buf); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + if (snd_ctl_elem_info_is_tlv_readable(info)) { + err = add_tlv_comments(handle, id, info, ctl, comment); + if (err < 0) + return err; + } + break; + } + case SND_CTL_ELEM_TYPE_INTEGER64: + { + long long min = snd_ctl_elem_info_get_min64(info); + long long max = snd_ctl_elem_info_get_max64(info); + long long step = snd_ctl_elem_info_get_step64(info); + if (step) + sprintf(buf, "%lli - %lli (step %lli)", min, max, step); + else + sprintf(buf, "%lli - %lli", min, max); + err = snd_config_string_add(comment, "range", buf); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + break; + } + case SND_CTL_ELEM_TYPE_ENUMERATED: + { + unsigned int items; + err = snd_config_compound_add(comment, "item", 1, &item); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + return err; + } + items = snd_ctl_elem_info_get_items(info); + for (idx = 0; idx < items; idx++) { + snd_ctl_elem_info_set_item(info, idx); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + error("snd_ctl_card_info: %s", snd_strerror(err)); + return err; + } + err = snd_config_string_add(item, num_str(idx), snd_ctl_elem_info_get_item_name(info)); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + } + break; + } + default: + break; + } + s = snd_ctl_elem_iface_name(snd_ctl_elem_info_get_interface(info)); + err = snd_config_string_add(control, "iface", s); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + if (device != 0) { + err = snd_config_integer_add(control, "device", device); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + } + if (subdevice != 0) { + err = snd_config_integer_add(control, "subdevice", subdevice); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + } + err = snd_config_string_add(control, "name", name); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + if (index != 0) { + err = snd_config_integer_add(control, "index", index); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + } + + switch (type) { + case SND_CTL_ELEM_TYPE_BYTES: + case SND_CTL_ELEM_TYPE_IEC958: + { + size_t size = type == SND_CTL_ELEM_TYPE_BYTES ? + count : sizeof(snd_aes_iec958_t); + char buf[size * 2 + 1]; + char *p = buf; + char *hex = "0123456789abcdef"; + const unsigned char *bytes = + (const unsigned char *)snd_ctl_elem_value_get_bytes(ctl); + for (idx = 0; idx < size; idx++) { + int v = bytes[idx]; + *p++ = hex[v >> 4]; + *p++ = hex[v & 0x0f]; + } + *p = '\0'; + err = snd_config_string_add(control, "value", buf); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + goto finish; + } + default: + break; + } + + if (count == 1) { + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + err = snd_config_string_add(control, "value", snd_ctl_elem_value_get_boolean(ctl, 0) ? "true" : "false"); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + goto finish; + case SND_CTL_ELEM_TYPE_INTEGER: + err = snd_config_integer_add(control, "value", snd_ctl_elem_value_get_integer(ctl, 0)); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + goto finish; + case SND_CTL_ELEM_TYPE_INTEGER64: + err = snd_config_integer64_add(control, "value", snd_ctl_elem_value_get_integer64(ctl, 0)); + if (err < 0) { + error("snd_config_integer64_add: %s", snd_strerror(err)); + return err; + } + goto finish; + case SND_CTL_ELEM_TYPE_ENUMERATED: + { + unsigned int v = snd_ctl_elem_value_get_enumerated(ctl, 0); + snd_config_t *c; + err = snd_config_search(item, num_str(v), &c); + if (err == 0) { + err = snd_config_get_string(c, &s); + assert(err == 0); + err = snd_config_string_add(control, "value", s); + } else { + err = snd_config_integer_add(control, "value", v); + } + if (err < 0) + error("snd_config add: %s", snd_strerror(err)); + goto finish; + } + default: + error("Unknown control type: %d\n", type); + return -EINVAL; + } + } + + err = snd_config_compound_add(control, "value", 1, &value); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + return err; + } + + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + for (idx = 0; idx < count; idx++) { + err = snd_config_string_add(value, num_str(idx), snd_ctl_elem_value_get_boolean(ctl, idx) ? "true" : "false"); + if (err < 0) { + error("snd_config_string_add: %s", snd_strerror(err)); + return err; + } + } + break; + case SND_CTL_ELEM_TYPE_INTEGER: + for (idx = 0; idx < count; idx++) { + err = snd_config_integer_add(value, num_str(idx), snd_ctl_elem_value_get_integer(ctl, idx)); + if (err < 0) { + error("snd_config_integer_add: %s", snd_strerror(err)); + return err; + } + } + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + for (idx = 0; idx < count; idx++) { + err = snd_config_integer64_add(value, num_str(idx), snd_ctl_elem_value_get_integer64(ctl, idx)); + if (err < 0) { + error("snd_config_integer64_add: %s", snd_strerror(err)); + return err; + } + } + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + for (idx = 0; idx < count; idx++) { + unsigned int v = snd_ctl_elem_value_get_enumerated(ctl, idx); + snd_config_t *c; + err = snd_config_search(item, num_str(v), &c); + if (err == 0) { + err = snd_config_get_string(c, &s); + assert(err == 0); + err = snd_config_string_add(value, num_str(idx), s); + } else { + err = snd_config_integer_add(value, num_str(idx), v); + } + if (err < 0) { + error("snd_config add: %s", snd_strerror(err)); + return err; + } + } + break; + default: + error("Unknown control type: %d\n", type); + return -EINVAL; + } + +finish: + err = snd_config_add(control, comment); + if (err < 0) { + error("snd_config_add: %s", snd_strerror(err)); + return err; + } + return 0; +} + +static int get_controls(int cardno, snd_config_t *top) +{ + snd_ctl_t *handle; + snd_ctl_card_info_t *info; + snd_config_t *state, *card, *control; + snd_ctl_elem_list_t *list; + snd_ctl_elem_id_t *elem_id; + unsigned int idx; + int err; + char name[32]; + unsigned int count; + const char *id; + snd_ctl_card_info_alloca(&info); + snd_ctl_elem_list_alloca(&list); + snd_ctl_elem_id_alloca(&elem_id); + + sprintf(name, "hw:%d", cardno); + err = snd_ctl_open(&handle, name, SND_CTL_READONLY); + if (err < 0) { + error("snd_ctl_open error: %s", snd_strerror(err)); + return err; + } + err = snd_ctl_card_info(handle, info); + if (err < 0) { + error("snd_ctl_card_info error: %s", snd_strerror(err)); + goto _close; + } + id = snd_ctl_card_info_get_id(info); + err = snd_config_search(top, "state", &state); + if (err == 0 && + snd_config_get_type(state) != SND_CONFIG_TYPE_COMPOUND) { + error("config state node is not a compound"); + err = -EINVAL; + goto _close; + } + if (err < 0) { + err = snd_config_compound_add(top, "state", 1, &state); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + goto _close; + } + } + err = snd_config_search(state, id, &card); + if (err == 0 && + snd_config_get_type(card) != SND_CONFIG_TYPE_COMPOUND) { + error("config state.%s node is not a compound", id); + err = -EINVAL; + goto _close; + } + if (err < 0) { + err = snd_config_compound_add(state, id, 0, &card); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + goto _close; + } + } + err = snd_config_search(card, "control", &control); + if (err == 0) { + err = snd_config_delete(control); + if (err < 0) { + error("snd_config_delete: %s", snd_strerror(err)); + goto _close; + } + } + err = snd_ctl_elem_list(handle, list); + if (err < 0) { + error("Cannot determine controls: %s", snd_strerror(err)); + goto _close; + } + count = snd_ctl_elem_list_get_count(list); + err = snd_config_compound_add(card, "control", count > 0, &control); + if (err < 0) { + error("snd_config_compound_add: %s", snd_strerror(err)); + goto _close; + } + if (count == 0) { + err = 0; + goto _close; + } + snd_ctl_elem_list_set_offset(list, 0); + if (snd_ctl_elem_list_alloc_space(list, count) < 0) { + error("No enough memory..."); + goto _close; + } + if ((err = snd_ctl_elem_list(handle, list)) < 0) { + error("Cannot determine controls (2): %s", snd_strerror(err)); + goto _free; + } + for (idx = 0; idx < count; ++idx) { + snd_ctl_elem_list_get_id(list, idx, elem_id); + err = get_control(handle, elem_id, control); + if (err < 0) + goto _free; + } + + err = 0; + _free: + snd_ctl_elem_list_free_space(list); + _close: + snd_ctl_close(handle); + return err; +} + +static long config_iface(snd_config_t *n) +{ + long i; + long long li; + snd_ctl_elem_iface_t idx; + const char *str; + switch (snd_config_get_type(n)) { + case SND_CONFIG_TYPE_INTEGER: + if (snd_config_get_integer(n, &i) < 0) + return -1; + return i; + case SND_CONFIG_TYPE_INTEGER64: + if (snd_config_get_integer64(n, &li) < 0) + return -1; + return li; + case SND_CONFIG_TYPE_STRING: + if (snd_config_get_string(n, &str) < 0) + return -1; + break; + default: + return -1; + } + for (idx = 0; idx <= SND_CTL_ELEM_IFACE_LAST; idx++) { + if (strcasecmp(snd_ctl_elem_iface_name(idx), str) == 0) + return idx; + } + return -1; +} + +static int config_bool(snd_config_t *n, int doit) +{ + const char *str; + long val; + long long lval; + + switch (snd_config_get_type(n)) { + case SND_CONFIG_TYPE_INTEGER: + if (snd_config_get_integer(n, &val) < 0) + return -1; + if (val < 0 || val > 1) + return -1; + return val; + case SND_CONFIG_TYPE_INTEGER64: + if (snd_config_get_integer64(n, &lval) < 0) + return -1; + if (lval < 0 || lval > 1) + return -1; + return (int) lval; + case SND_CONFIG_TYPE_STRING: + if (snd_config_get_string(n, &str) < 0) + return -1; + break; + case SND_CONFIG_TYPE_COMPOUND: + if (!force_restore || !doit) + return -1; + n = snd_config_iterator_entry(snd_config_iterator_first(n)); + return config_bool(n, doit); + default: + return -1; + } + if (strcmp(str, "on") == 0 || strcmp(str, "true") == 0) + return 1; + if (strcmp(str, "off") == 0 || strcmp(str, "false") == 0) + return 0; + return -1; +} + +static int config_enumerated(snd_config_t *n, snd_ctl_t *handle, + snd_ctl_elem_info_t *info, int doit) +{ + const char *str; + long val; + long long lval; + unsigned int idx, items; + + switch (snd_config_get_type(n)) { + case SND_CONFIG_TYPE_INTEGER: + if (snd_config_get_integer(n, &val) < 0) + return -1; + return val; + case SND_CONFIG_TYPE_INTEGER64: + if (snd_config_get_integer64(n, &lval) < 0) + return -1; + return (int) lval; + case SND_CONFIG_TYPE_STRING: + if (snd_config_get_string(n, &str) < 0) + return -1; + break; + case SND_CONFIG_TYPE_COMPOUND: + if (!force_restore || !doit) + return -1; + n = snd_config_iterator_entry(snd_config_iterator_first(n)); + return config_enumerated(n, handle, info, doit); + default: + return -1; + } + items = snd_ctl_elem_info_get_items(info); + for (idx = 0; idx < items; idx++) { + int err; + snd_ctl_elem_info_set_item(info, idx); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + error("snd_ctl_elem_info: %s", snd_strerror(err)); + return err; + } + if (strcmp(str, snd_ctl_elem_info_get_item_name(info)) == 0) + return idx; + } + return -1; +} + +static int config_integer(snd_config_t *n, long *val, int doit) +{ + int err = snd_config_get_integer(n, val); + if (err < 0 && force_restore && doit) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND) + return err; + n = snd_config_iterator_entry(snd_config_iterator_first(n)); + return config_integer(n, val, doit); + } + return err; +} + +static int config_integer64(snd_config_t *n, long long *val, int doit) +{ + int err = snd_config_get_integer64(n, val); + if (err < 0 && force_restore && doit) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND) + return err; + n = snd_config_iterator_entry(snd_config_iterator_first(n)); + return config_integer64(n, val, doit); + } + return err; +} + +static int check_comment_access(snd_config_t *conf, const char *str) +{ + snd_config_iterator_t i, next; + + snd_config_for_each(i, next, conf) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *id, *s; + if (snd_config_get_id(n, &id) < 0) + continue; + if (strcmp(id, "access") == 0) { + if (snd_config_get_string(n, &s) < 0) + return 0; + if (strstr(s, str)) + return 1; + } + } + return 0; +} + +/* + * get the item type from the given comment config + */ +static int get_comment_type(snd_config_t *n) +{ + static const snd_ctl_elem_type_t types[] = { + SND_CTL_ELEM_TYPE_BOOLEAN, + SND_CTL_ELEM_TYPE_INTEGER, + SND_CTL_ELEM_TYPE_ENUMERATED, + SND_CTL_ELEM_TYPE_BYTES, + SND_CTL_ELEM_TYPE_IEC958, + SND_CTL_ELEM_TYPE_INTEGER64, + }; + const char *type; + unsigned int i; + + if (snd_config_get_string(n, &type) < 0) + return -EINVAL; + for (i = 0; i < ARRAY_SIZE(types); ++i) + if (strcmp(type, snd_ctl_elem_type_name(types[i])) == 0) + return types[i]; + return -EINVAL; +} + +/* + * get the value range from the given comment config + */ +static int get_comment_range(snd_config_t *n, int ctype, + long *imin, long *imax, long *istep) +{ + const char *s; + int err; + + if (snd_config_get_string(n, &s) < 0) + return -EINVAL; + switch (ctype) { + case SND_CTL_ELEM_TYPE_INTEGER: + err = sscanf(s, "%li - %li (step %li)", imin, imax, istep); + if (err != 3) { + istep = 0; + err = sscanf(s, "%li - %li", imin, imax); + if (err != 2) + return -EINVAL; + } + break; + default: + return -EINVAL; + } + return 0; +} + +struct string_array { + unsigned int count; + const char **strings; +}; + +static int get_comment_items(snd_config_t *n, struct string_array *items) +{ + snd_config_iterator_t it, next; + unsigned int i; + int err; + + snd_config_for_each(it, next, n) { + snd_config_t *item = snd_config_iterator_entry(it); + const char *id; + unsigned int numid; + + if (snd_config_get_id(item, &id) < 0) + return -EINVAL; + numid = atoi(id); + if (numid > 999999) + return -EINVAL; + + if (numid >= items->count) { + const char **strings = realloc(items->strings, (numid + 1) * sizeof(char *)); + if (!strings) + return -ENOMEM; + for (i = items->count; i < numid + 1; ++i) + strings[i] = NULL; + items->count = numid + 1; + items->strings = strings; + } + err = snd_config_get_string(item, &items->strings[numid]); + if (err < 0) + return err; + } + + for (i = 0; i < items->count; ++i) + if (!items->strings[i]) + return -EINVAL; + + return 0; +} + +static int add_user_control(snd_ctl_t *handle, snd_ctl_elem_info_t *info, snd_config_t *conf) +{ + snd_ctl_elem_id_t *id; + snd_config_iterator_t i, next; + long imin, imax, istep; + snd_ctl_elem_type_t ctype; + unsigned int count; + struct string_array enum_items; + int err; + unsigned int *tlv; + + imin = imax = istep = 0; + count = 0; + ctype = SND_CTL_ELEM_TYPE_NONE; + enum_items.count = 0; + enum_items.strings = NULL; + tlv = NULL; + snd_config_for_each(i, next, conf) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *id; + if (snd_config_get_id(n, &id) < 0) + continue; + if (strcmp(id, "type") == 0) { + err = get_comment_type(n); + if (err < 0) + goto error; + ctype = err; + continue; + } + if (strcmp(id, "range") == 0) { + err = get_comment_range(n, ctype, &imin, &imax, &istep); + if (err < 0) + goto error; + continue; + } + if (strcmp(id, "count") == 0) { + long v; + if ((err = snd_config_get_integer(n, &v)) < 0) + goto error; + count = v; + continue; + } + if (strcmp(id, "item") == 0) { + err = get_comment_items(n, &enum_items); + if (err < 0) + goto error; + continue; + } + if (strcmp(id, "tlv") == 0) { + const char *s; + if ((err = snd_config_get_string(n, &s)) < 0) + goto error; + if (tlv) + free(tlv); + if ((tlv = str_to_tlv(s)) == NULL) { + err = -EINVAL; + goto error; + } + continue; + } + } + + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_info_get_id(info, id); + if (count <= 0) + count = 1; + switch (ctype) { + case SND_CTL_ELEM_TYPE_INTEGER: + if (imin > imax || istep > imax - imin) { + err = -EINVAL; + goto error; + } + err = snd_ctl_elem_add_integer(handle, id, count, imin, imax, istep); + if (err < 0) + goto error; + if (tlv) + snd_ctl_elem_tlv_write(handle, id, tlv); + break; + case SND_CTL_ELEM_TYPE_BOOLEAN: + err = snd_ctl_elem_add_boolean(handle, id, count); + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + err = snd_ctl_elem_add_enumerated(handle, id, count, + enum_items.count, enum_items.strings); + break; + case SND_CTL_ELEM_TYPE_IEC958: + err = snd_ctl_elem_add_iec958(handle, id); + break; + default: + err = -EINVAL; + break; + } + + error: + free(tlv); + free(enum_items.strings); + if (err < 0) + return err; + return snd_ctl_elem_info(handle, info); +} + +/* + * check whether the config item has the same of compatible type + */ +static int check_comment_type(snd_config_t *conf, int type) +{ + snd_config_t *n; + int ctype; + + if (snd_config_search(conf, "type", &n) < 0) + return 0; /* not defined */ + ctype = get_comment_type(n); + if (ctype == type) + return 0; + if ((ctype == SND_CTL_ELEM_TYPE_BOOLEAN || + ctype == SND_CTL_ELEM_TYPE_INTEGER || + ctype == SND_CTL_ELEM_TYPE_INTEGER64 || + ctype == SND_CTL_ELEM_TYPE_ENUMERATED) && + (type == SND_CTL_ELEM_TYPE_BOOLEAN || + type == SND_CTL_ELEM_TYPE_INTEGER || + type == SND_CTL_ELEM_TYPE_INTEGER64 || + type == SND_CTL_ELEM_TYPE_ENUMERATED)) + return 0; /* OK, compatible */ + return -EINVAL; +} + +/* + * convert from an old value to a new value with the same dB level + */ +static int convert_to_new_db(snd_config_t *value, long omin, long omax, + long nmin, long nmax, + long odbmin, long odbmax, + snd_config_t *comment, const char *index, + snd_ctl_t *device, snd_ctl_elem_id_t *id, + int doit) +{ + snd_config_t *db_node; + long db, val; + int err; + + if (snd_config_searchv(comment, &db_node, "dbvalue", index, NULL) < 0 || + snd_config_get_integer(db_node, &db) < 0) { + err = config_integer(value, &val, doit); + if (err < 0) + return err; + if (val < omin || val > omax) + return -EINVAL; + db = ((val - omin) * (odbmax - odbmin)) / (omax - omin) + odbmin; + } + + err = snd_ctl_convert_from_dB(device, id, db, &val, db > 0); + if (err < 0) + return err; + if (val < nmin) + val = nmin; + else if (val > nmax) + val = nmax; + return snd_config_set_integer(value, val); +} + +/* + * compare the current value range with the old range in comments. + * also, if dB information is available, try to compare them. + * if any change occurs, try to keep the same dB level. + */ +static int check_comment_range(snd_ctl_t *handle, snd_config_t *conf, + snd_ctl_elem_info_t *info, snd_config_t *value, + int doit) +{ + snd_config_t *n; + long omin, omax, ostep; + long nmin, nmax; + long odbmin, odbmax; + long ndbmin, ndbmax; + snd_ctl_elem_id_t *id; + + if (snd_config_search(conf, "range", &n) < 0) + return 0; + if (get_comment_range(n, SND_CTL_ELEM_TYPE_INTEGER, + &omin, &omax, &ostep) < 0) + return 0; + nmin = snd_ctl_elem_info_get_min(info); + nmax = snd_ctl_elem_info_get_max(info); + if (omin != nmin && omax != nmax) { + /* Hey, the range mismatches */ + if (!force_restore || !doit) + return -EINVAL; + } + if (omin >= omax || nmin >= nmax) + return 0; /* invalid values */ + + if (snd_config_search(conf, "dbmin", &n) < 0) + return 0; + if (config_integer(n, &odbmin, doit) < 0) + return 0; + if (snd_config_search(conf, "dbmax", &n) < 0) + return 0; + if (config_integer(n, &odbmax, doit) < 0) + return 0; + if (odbmin >= odbmax) + return 0; /* invalid values */ + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_info_get_id(info, id); + if (snd_ctl_get_dB_range(handle, id, &ndbmin, &ndbmax) < 0) + return 0; + if (ndbmin >= ndbmax) + return 0; /* invalid values */ + if (omin == nmin && omax == nmax && + odbmin == ndbmin && odbmax == ndbmax) + return 0; /* OK, identical one */ + + /* Let's guess the current value from dB range */ + if (snd_config_get_type(value) == SND_CONFIG_TYPE_COMPOUND) { + snd_config_iterator_t i, next; + snd_config_for_each(i, next, value) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *idxstr; + if (snd_config_get_id(n, &idxstr) < 0) + continue; + convert_to_new_db(n, omin, omax, nmin, nmax, + odbmin, odbmax, conf, idxstr, + handle, id, doit); + } + } else + convert_to_new_db(value, omin, omax, nmin, nmax, + odbmin, odbmax, conf, "0", + handle, id, doit); + return 0; +} + +static int restore_config_value(snd_ctl_t *handle, snd_ctl_elem_info_t *info, + snd_ctl_elem_type_t type, + snd_config_t *value, + snd_ctl_elem_value_t *ctl, int idx, + int doit) +{ + long val; + long long lval; + int err; + + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + val = config_bool(value, doit); + if (val >= 0) { + snd_ctl_elem_value_set_boolean(ctl, idx, val); + return 1; + } + break; + case SND_CTL_ELEM_TYPE_INTEGER: + err = config_integer(value, &val, doit); + if (err == 0) { + snd_ctl_elem_value_set_integer(ctl, idx, val); + return 1; + } + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + err = config_integer64(value, &lval, doit); + if (err == 0) { + snd_ctl_elem_value_set_integer64(ctl, idx, lval); + return 1; + } + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + val = config_enumerated(value, handle, info, doit); + if (val >= 0) { + snd_ctl_elem_value_set_enumerated(ctl, idx, val); + return 1; + } + break; + case SND_CTL_ELEM_TYPE_BYTES: + case SND_CTL_ELEM_TYPE_IEC958: + break; + default: + cerror(doit, "Unknow control type: %d", type); + return -EINVAL; + } + return 0; +} + +static int restore_config_value2(snd_ctl_t *handle, snd_ctl_elem_info_t *info, + snd_ctl_elem_type_t type, + snd_config_t *value, + snd_ctl_elem_value_t *ctl, int idx, + unsigned int numid, int doit) +{ + int err = restore_config_value(handle, info, type, value, ctl, idx, doit); + long val; + + if (err != 0) + return err; + switch (type) { + case SND_CTL_ELEM_TYPE_BYTES: + case SND_CTL_ELEM_TYPE_IEC958: + err = snd_config_get_integer(value, &val); + if (err < 0 || val < 0 || val > 255) { + cerror(doit, "bad control.%d.value.%d content", numid, idx); + return force_restore && doit ? 0 : -EINVAL; + } + snd_ctl_elem_value_set_byte(ctl, idx, val); + return 1; + default: + break; + } + return 0; +} + +static int parse_config_id(snd_config_t *control, snd_ctl_elem_id_t *id, + snd_config_t **comment, snd_config_t **value, int doit) +{ + snd_config_iterator_t i, next; + const char *numid; + snd_ctl_elem_iface_t iface = -1; + long device = -1; + long subdevice = -1; + const char *name = NULL; + long index = -1; + + snd_ctl_elem_id_clear(id); + if (snd_config_get_id(control, &numid) < 0) + return -EINVAL; + snd_config_for_each(i, next, control) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *fld; + if (snd_config_get_id(n, &fld) < 0) + continue; + if (strcmp(fld, "comment") == 0) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND) { + cerror(doit, "control.%s.%s is invalid", numid, fld); + return -EINVAL; + } + if (comment) + *comment = n; + continue; + } + if (strcmp(fld, "iface") == 0) { + iface = (snd_ctl_elem_iface_t)config_iface(n); + if (iface < 0) + return -EINVAL; + continue; + } + if (strcmp(fld, "device") == 0) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_INTEGER) { + cerror(doit, "control.%s.%s is invalid", numid, fld); + return -EINVAL; + } + if (snd_config_get_integer(n, &device) < 0) + return -EINVAL; + continue; + } + if (strcmp(fld, "subdevice") == 0) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_INTEGER) { + cerror(doit, "control.%s.%s is invalid", numid, fld); + return -EINVAL; + } + if (snd_config_get_integer(n, &subdevice) < 0) + return -EINVAL; + continue; + } + if (strcmp(fld, "name") == 0) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_STRING) { + cerror(doit, "control.%s.%s is invalid", numid, fld); + return -EINVAL; + } + if (snd_config_get_string(n, &name) < 0) + return -EINVAL; + continue; + } + if (strcmp(fld, "index") == 0) { + if (snd_config_get_type(n) != SND_CONFIG_TYPE_INTEGER) { + cerror(doit, "control.%s.%s is invalid", numid, fld); + return -EINVAL; + } + if (snd_config_get_integer(n, &index) < 0) + return -EINVAL; + continue; + } + if (strcmp(fld, "value") == 0) { + if (value) + *value = n; + continue; + } + cerror(doit, "unknown control.%s.%s field", numid, fld); + } + if (device < 0) + device = 0; + if (subdevice < 0) + subdevice = 0; + if (index < 0) + index = 0; + snd_ctl_elem_id_set_interface(id, iface); + snd_ctl_elem_id_set_device(id, device); + snd_ctl_elem_id_set_subdevice(id, subdevice); + snd_ctl_elem_id_set_name(id, name); + snd_ctl_elem_id_set_index(id, index); + return 0; +} + +static int set_control(snd_ctl_t *handle, snd_config_t *control, int doit) +{ + snd_ctl_elem_value_t *ctl; + snd_ctl_elem_info_t *info; + snd_config_iterator_t i, next; + snd_ctl_elem_id_t *elem_id, *elem_id1; + unsigned int numid; + snd_ctl_elem_type_t type; + unsigned int count; + snd_config_t *value = NULL; + snd_config_t *comment = NULL; + unsigned int idx; + int err; + char *set; + const char *id; + snd_ctl_elem_id_alloca(&elem_id); + snd_ctl_elem_id_alloca(&elem_id1); + snd_ctl_elem_value_alloca(&ctl); + snd_ctl_elem_info_alloca(&info); + if (snd_config_get_type(control) != SND_CONFIG_TYPE_COMPOUND) { + cerror(doit, "control is not a compound"); + return -EINVAL; + } + err = snd_config_get_id(control, &id); + if (err < 0) { + cerror(doit, "unable to get id"); + return -EINVAL; + } + numid = atoi(id); + err = parse_config_id(control, elem_id, &comment, &value, doit); + if (err < 0) + return err; + err = -EINVAL; + if (!force_restore) { + snd_ctl_elem_info_set_numid(info, numid); + err = snd_ctl_elem_info(handle, info); + } + id = snd_ctl_elem_id_get_name(elem_id); + if (err < 0 && id && id[0]) { + snd_ctl_elem_info_set_id(info, elem_id); + err = snd_ctl_elem_info(handle, info); + if (err < 0 && comment && check_comment_access(comment, "user")) { + err = add_user_control(handle, info, comment); + if (err < 0) { + cerror(doit, "failed to add user control #%d (%s)", + numid, snd_strerror(err)); + return err; + } + } + } + if (err < 0) { + cerror(doit, "failed to obtain info for control #%d (%s)", numid, snd_strerror(err)); + return -ENOENT; + } + count = snd_ctl_elem_info_get_count(info); + snd_ctl_elem_info_get_id(info, elem_id1); + type = snd_ctl_elem_info_get_type(info); + err = 0; + if (err |= !force_restore && numid != snd_ctl_elem_id_get_numid(elem_id1)) + cerror(doit, "warning: numid mismatch (%u/%u) for control #%d", + (unsigned int)numid, snd_ctl_elem_id_get_numid(elem_id1), numid); + if (err |= snd_ctl_elem_id_get_interface(elem_id) != snd_ctl_elem_id_get_interface(elem_id1)) + cerror(doit, "warning: iface mismatch (%d/%d) for control #%d", + (int)snd_ctl_elem_id_get_interface(elem_id), + (int)snd_ctl_elem_id_get_interface(elem_id1), + numid); + if (err |= snd_ctl_elem_id_get_device(elem_id) != snd_ctl_elem_id_get_device(elem_id1)) + cerror(doit, "warning: device mismatch (%u/%u) for control #%d", + snd_ctl_elem_id_get_device(elem_id), + snd_ctl_elem_id_get_device(elem_id1), + numid); + if (err |= snd_ctl_elem_id_get_subdevice(elem_id) != snd_ctl_elem_id_get_subdevice(elem_id1)) + cerror(doit, "warning: subdevice mismatch (%u/%%u) for control #%d", + snd_ctl_elem_id_get_subdevice(elem_id), + snd_ctl_elem_id_get_subdevice(elem_id1), + numid); + if (err |= strcmp(snd_ctl_elem_id_get_name(elem_id), snd_ctl_elem_id_get_name(elem_id1))) + cerror(doit, "warning: name mismatch (%s/%s) for control #%d", + snd_ctl_elem_id_get_name(elem_id), + snd_ctl_elem_id_get_name(elem_id1), + numid); + if (err |= snd_ctl_elem_id_get_index(elem_id) != snd_ctl_elem_id_get_index(elem_id1)) + cerror(doit, "warning: index mismatch (%u/%u) for control #%d", + snd_ctl_elem_id_get_index(elem_id), + snd_ctl_elem_id_get_index(elem_id1), + numid); + if (err) { + cerror(doit, "failed to obtain info for control #%d (%s)", numid, snd_strerror(err)); + return -ENOENT; + } + + if (comment) { + if (check_comment_type(comment, type) < 0) + cerror(doit, "incompatible field type for control #%d", numid); + if (type == SND_CTL_ELEM_TYPE_INTEGER) { + if (check_comment_range(handle, comment, info, value, doit) < 0) { + cerror(doit, "value range mismatch for control #%d", + numid); + return -EINVAL; + } + } + /* inactive controls are not restored */ + if (comment && check_comment_access(comment, "inactive")) + return 0; + } + + if (snd_ctl_elem_info_is_inactive(info) || + !snd_ctl_elem_info_is_writable(info)) + return 0; + snd_ctl_elem_value_set_numid(ctl, snd_ctl_elem_id_get_numid(elem_id1)); + + if (count == 1) { + err = restore_config_value(handle, info, type, value, ctl, 0, doit); + if (err < 0) + return err; + if (err > 0) + goto _ok; + } + switch (type) { + case SND_CTL_ELEM_TYPE_BYTES: + case SND_CTL_ELEM_TYPE_IEC958: + { + const char *buf; + err = snd_config_get_string(value, &buf); + if (err >= 0) { + int c1 = 0; + int len = strlen(buf); + unsigned int idx = 0; + int size = type == SND_CTL_ELEM_TYPE_BYTES ? + count : sizeof(snd_aes_iec958_t); + if (size * 2 != len) { + cerror(doit, "bad control.%d.value contents\n", numid); + return -EINVAL; + } + while (*buf) { + int c = *buf++; + if ((c = hextodigit(c)) < 0) { + cerror(doit, "bad control.%d.value contents\n", numid); + return -EINVAL; + } + if (idx % 2 == 1) + snd_ctl_elem_value_set_byte(ctl, idx / 2, c1 << 4 | c); + else + c1 = c; + idx++; + } + goto _ok; + } + } + default: + break; + } + if (snd_config_get_type(value) != SND_CONFIG_TYPE_COMPOUND) { + if (!force_restore || !doit) { + cerror(doit, "bad control.%d.value type", numid); + return -EINVAL; + } + for (idx = 0; idx < count; ++idx) { + err = restore_config_value2(handle, info, type, value, + ctl, idx, numid, doit); + if (err < 0) + return err; + } + goto _ok; + } + + set = (char*) alloca(count); + memset(set, 0, count); + snd_config_for_each(i, next, value) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *id; + if (snd_config_get_id(n, &id) < 0) + continue; + idx = atoi(id); + if (idx >= count || set[idx]) { + cerror(doit, "bad control.%d.value index", numid); + if (!force_restore || !doit) + return -EINVAL; + continue; + } + err = restore_config_value2(handle, info, type, n, + ctl, idx, numid, doit); + if (err < 0) + return err; + if (err > 0) + set[idx] = 1; + } + for (idx = 0; idx < count; ++idx) { + if (!set[idx]) { + cerror(doit, "control.%d.value.%d is not specified", numid, idx); + if (!force_restore || !doit) + return -EINVAL; + } + } + + _ok: + err = doit ? snd_ctl_elem_write(handle, ctl) : 0; + if (err < 0) { + char *s = snd_ctl_ascii_elem_id_get(elem_id1); + error("Cannot write control '%s' : %s", s, snd_strerror(err)); + free(s); + return err; + } else if (debugflag) { + char *s = snd_ctl_ascii_elem_id_get(elem_id1); + dbg("control '%s' restored", s); + free(s); + } + return 0; +} + +static int set_controls(int card, snd_config_t *top, int doit) +{ + snd_ctl_t *handle; + snd_ctl_card_info_t *info; + snd_ctl_elem_list_t *list; + snd_ctl_elem_info_t *elem_info; + snd_ctl_elem_id_t *elem_id; + snd_config_t *control; + snd_config_iterator_t i, next; + int err, controls1 = -1, controls2 = -1; + unsigned int idx, count = 0; + char name[32], tmpid[16]; + const char *id; + snd_ctl_card_info_alloca(&info); + snd_ctl_elem_list_alloca(&list); + snd_ctl_elem_info_alloca(&elem_info); + snd_ctl_elem_id_alloca(&elem_id); + sprintf(name, "hw:%d", card); + dbg("device='%s', doit=%i", name, doit); + err = snd_ctl_open(&handle, name, 0); + if (err < 0) { + error("snd_ctl_open error: %s", snd_strerror(err)); + return err; + } + err = snd_ctl_card_info(handle, info); + if (err < 0) { + error("snd_ctl_card_info error: %s", snd_strerror(err)); + goto _close; + } + id = snd_ctl_card_info_get_id(info); + dbg("card-info-id: '%s'", id); + err = snd_config_searchv(top, &control, "state", id, "control", 0); + if (err < 0) { + if (force_restore) { + sprintf(tmpid, "card%d", card); + err = snd_config_searchv(top, &control, "state", tmpid, "control", 0); + if (! err) + id = tmpid; + } + if (err < 0) { + fprintf(stderr, "No state is present for card %s\n", id); + goto _close; + } + id = tmpid; + } + if (snd_config_get_type(control) != SND_CONFIG_TYPE_COMPOUND) { + cerror(doit, "state.%s.control is not a compound\n", id); + return -EINVAL; + } + controls1 = 0; + snd_config_for_each(i, next, control) { + snd_config_t *n = snd_config_iterator_entry(i); + err = set_control(handle, n, doit); + if (err < 0 && (!force_restore || !doit)) + goto _close; + controls1++; + } + + if (doit) + goto _close; + + err = snd_ctl_elem_list(handle, list); + if (err < 0) { + error("Cannot determine controls: %s", snd_strerror(err)); + goto _close; + } + count = snd_ctl_elem_list_get_count(list); + dbg("list count: %u", count); + if (count == 0) + goto _check; + snd_ctl_elem_list_set_offset(list, 0); + if (snd_ctl_elem_list_alloc_space(list, count) < 0) { + error("No enough memory..."); + goto _close; + } + if ((err = snd_ctl_elem_list(handle, list)) < 0) { + error("Cannot determine controls (2): %s", snd_strerror(err)); + goto _free; + } + controls2 = 0; + /* skip non-readable elements */ + for (idx = 0; idx < count; ++idx) { + snd_ctl_elem_info_clear(elem_info); + snd_ctl_elem_list_get_id(list, idx, elem_id); + snd_ctl_elem_info_set_id(elem_info, elem_id); + if (snd_ctl_elem_info(handle, elem_info) == 0) { + if (!snd_ctl_elem_info_is_readable(elem_info)) + continue; + controls2++; + } + } + + /* check if we have additional controls in driver */ + /* in this case we should go through init procedure */ + _check: + dbg("controls1=%i controls2=%i", controls1, controls2); + if (controls1 >= 0 && controls1 != controls2) { + /* not very informative */ + /* but value is used for check only */ + err = -EAGAIN; + dbg("config controls mismatch with hardware?"); + } + + _free: + if (count > 0) + snd_ctl_elem_list_free_space(list); + _close: + snd_ctl_close(handle); + dbg("result code: %i", err); + return err; +} + +int save_state(const char *file, const char *cardname) +{ + int err; + snd_config_t *config; + snd_input_t *in; + snd_output_t *out; + int stdio; + char *nfile = NULL; + int lock_fd = -EINVAL; + struct snd_card_iterator iter; + + err = snd_config_top(&config); + if (err < 0) { + error("snd_config_top error: %s", snd_strerror(err)); + return err; + } + stdio = !strcmp(file, "-"); + if (!stdio) { + nfile = malloc(strlen(file) + 5); + if (nfile == NULL) { + error("No enough memory..."); + err = -ENOMEM; + goto out; + } + strcpy(nfile, file); + strcat(nfile, ".new"); + lock_fd = state_lock(file, LOCK_TIMEOUT); + if (lock_fd < 0) { + err = lock_fd; + goto out; + } + } + if (!stdio && (err = snd_input_stdio_open(&in, file, "r")) >= 0) { + err = snd_config_load(config, in); + snd_input_close(in); +#if 0 + if (err < 0) { + error("snd_config_load error: %s", snd_strerror(err)); + goto out; + } +#endif + } + + err = snd_card_iterator_sinit(&iter, cardname); + if (err < 0) + goto out; + while (snd_card_iterator_next(&iter)) { + if ((err = get_controls(iter.card, config))) + goto out; + } + if (iter.first) { + err = snd_card_iterator_error(&iter); + goto out; + } + + if (stdio) { + err = snd_output_stdio_attach(&out, stdout, 0); + } else { + err = snd_output_stdio_open(&out, nfile, "w"); + } + if (err < 0) { + error("Cannot open %s for writing: %s", file, snd_strerror(err)); + err = -errno; + goto out; + } + err = snd_config_save(config, out); + snd_output_close(out); + if (err < 0) { + error("snd_config_save: %s", snd_strerror(err)); + } else if (nfile) { + err = rename(nfile, file); + if (err < 0) + error("rename failed: %s (%s)", strerror(-err), file); + } +out: + if (!stdio && lock_fd >= 0) + state_unlock(lock_fd, file); + free(nfile); + snd_config_delete(config); + snd_config_update_free_global(); + return err; +} + +int load_state(const char *cfgdir, const char *file, + const char *initfile, int initflags, + const char *cardname, int do_init) +{ + int err, finalerr = 0, open_failed, lock_fd; + struct snd_card_iterator iter; + snd_config_t *config; + const char *cardname1; + + config = NULL; + err = load_configuration(file, &config, &open_failed); + if (err < 0 && !open_failed) + return err; + + if (open_failed) { + error("Cannot open %s for reading: %s", file, snd_strerror(err)); + finalerr = err; + + err = snd_card_iterator_sinit(&iter, cardname); + if (err < 0) + return err; + while ((cardname1 = snd_card_iterator_next(&iter)) != NULL) { + if (!do_init) + break; + lock_fd = card_lock(iter.card, LOCK_TIMEOUT); + if (lock_fd < 0) { + finalerr = lock_fd; + initfailed(iter.card, "lock", err); + continue; + } + err = init(cfgdir, initfile, initflags | FLAG_UCM_FBOOT | FLAG_UCM_BOOT, cardname1); + card_unlock(lock_fd, iter.card); + if (err < 0) { + finalerr = err; + initfailed(iter.card, "init", err); + } + initfailed(iter.card, "restore", -ENOENT); + } + err = finalerr; + if (iter.first) + err = 0; /* no cards, no error code */ + goto out; + } + + err = snd_card_iterator_sinit(&iter, cardname); + if (err < 0) + goto out; + while ((cardname1 = snd_card_iterator_next(&iter)) != NULL) { + lock_fd = card_lock(iter.card, LOCK_TIMEOUT); + if (lock_fd < 0) { + initfailed(iter.card, "lock", lock_fd); + finalerr = lock_fd; + continue; + } + /* error is ignored */ + init_ucm(initflags | FLAG_UCM_FBOOT, iter.card); + /* do a check if controls matches state file */ + if (do_init && set_controls(iter.card, config, 0)) { + err = init(cfgdir, initfile, initflags | FLAG_UCM_BOOT, cardname1); + if (err < 0) { + initfailed(iter.card, "init", err); + finalerr = err; + } + } + if ((err = set_controls(iter.card, config, 1))) { + if (!force_restore) + finalerr = err; + initfailed(iter.card, "restore", err); + } + card_unlock(lock_fd, iter.card); + } + err = finalerr ? finalerr : snd_card_iterator_error(&iter); +out: + if (config) + snd_config_delete(config); + snd_config_update_free_global(); + return err; +} diff --git a/alsactl/utils.c b/alsactl/utils.c new file mode 100644 index 0000000..d8cbf53 --- /dev/null +++ b/alsactl/utils.c @@ -0,0 +1,334 @@ +/* + * Advanced Linux Sound Architecture Control Program - Support routines + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "alsactl.h" + +int file_map(const char *filename, char **buf, size_t *bufsize) +{ + struct stat stats; + int fd; + + fd = open(filename, O_RDONLY); + if (fd < 0) { + return -1; + } + + if (fstat(fd, &stats) < 0) { + close(fd); + return -1; + } + + *buf = mmap(NULL, stats.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (*buf == MAP_FAILED) { + close(fd); + return -1; + } + *bufsize = stats.st_size; + + close(fd); + + return 0; +} + +void file_unmap(void *buf, size_t bufsize) +{ + munmap(buf, bufsize); +} + +size_t line_width(const char *buf, size_t bufsize, size_t pos) +{ + int esc = 0; + size_t count; + + for (count = pos; count < bufsize; count++) { + if (!esc && buf[count] == '\n') + break; + esc = buf[count] == '\\'; + } + + return count - pos; +} + +void initfailed(int cardnumber, const char *reason, int exitcode) +{ + int fp; + char *str; + char sexitcode[16]; + + if (statefile == NULL) + return; + if (snd_card_get_name(cardnumber, &str) < 0) + return; + sprintf(sexitcode, "%i", exitcode); + fp = open(statefile, O_WRONLY|O_CREAT|O_APPEND, 0644); + (void)write(fp, str, strlen(str)); + (void)write(fp, ":", 1); + (void)write(fp, reason, strlen(reason)); + (void)write(fp, ":", 1); + (void)write(fp, sexitcode, strlen(sexitcode)); + (void)write(fp, "\n", 1); + close(fp); + free(str); +} + +static void syslog_(int prio, const char *fcn, long line, + const char *fmt, va_list ap) +{ + char buf[1024]; + + snprintf(buf, sizeof(buf), "%s: %s:%ld: ", command, fcn, line); + buf[sizeof(buf)-1] = '\0'; + vsnprintf(buf + strlen(buf), sizeof(buf)-strlen(buf), fmt, ap); + buf[sizeof(buf)-1] = '\0'; + syslog(prio, "%s", buf); +} + +void info_(const char *fcn, long line, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (use_syslog) { + syslog_(LOG_INFO, fcn, line, fmt, ap); + } else { + fprintf(stdout, "%s: %s:%ld: ", command, fcn, line); + vfprintf(stdout, fmt, ap); + putc('\n', stdout); + } + va_end(ap); +} + +void error_(const char *fcn, long line, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (use_syslog) { + syslog_(LOG_ERR, fcn, line, fmt, ap); + } else { + fprintf(stderr, "%s: %s:%ld: ", command, fcn, line); + vfprintf(stderr, fmt, ap); + putc('\n', stderr); + } + va_end(ap); +} + +void cerror_(const char *fcn, long line, int cond, const char *fmt, ...) +{ + va_list ap; + + if (!cond && !debugflag) + return; + va_start(ap, fmt); + if (use_syslog) { + syslog_(LOG_ERR, fcn, line, fmt, ap); + } else { + fprintf(stderr, "%s: %s:%ld: ", command, fcn, line); + vfprintf(stderr, fmt, ap); + putc('\n', stderr); + } + va_end(ap); +} + +void dbg_(const char *fcn, long line, const char *fmt, ...) +{ + va_list ap; + + if (!debugflag) + return; + va_start(ap, fmt); + if (use_syslog) { + syslog_(LOG_DEBUG, fcn, line, fmt, ap); + } else { + fprintf(stderr, "%s: %s:%ld: ", command, fcn, line); + vfprintf(stderr, fmt, ap); + putc('\n', stderr); + } + va_end(ap); +} + +void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...) +{ + char buf[2048]; + va_list arg; + + va_start(arg, fmt); + vsnprintf(buf, sizeof(buf), fmt, arg); + va_end(arg); + if (use_syslog) + syslog(LOG_ERR, "alsa-lib %s:%i:(%s) %s%s%s\n", file, line, function, + buf, err ? ": " : "", err ? snd_strerror(err) : ""); + else + fprintf(stderr, "alsa-lib %s:%i:(%s) %s%s%s\n", file, line, function, + buf, err ? ": " : "", err ? snd_strerror(err) : ""); +} + +int load_configuration(const char *file, snd_config_t **top, int *open_failed) +{ + snd_config_t *config; + snd_input_t *in; + int err, stdio_flag, lock_fd = -EINVAL; + + *top = NULL; + if (open_failed) + *open_failed = 0; + err = snd_config_top(&config); + if (err < 0) { + error("snd_config_top error: %s", snd_strerror(err)); + return err; + } + stdio_flag = !strcmp(file, "-"); + if (stdio_flag) { + err = snd_input_stdio_attach(&in, stdin, 0); + } else { + lock_fd = state_lock(file, LOCK_TIMEOUT); + err = lock_fd >= 0 ? snd_input_stdio_open(&in, file, "r") : lock_fd; + } + if (err < 0) { + if (open_failed) + *open_failed = 1; + goto out; + } + err = snd_config_load(config, in); + snd_input_close(in); + if (err < 0) { + error("snd_config_load error: %s", snd_strerror(err)); +out: + if (lock_fd >= 0) + state_unlock(lock_fd, file); + snd_config_delete(config); + snd_config_update_free_global(); + return err; + } else { + if (lock_fd >= 0) + state_unlock(lock_fd, file); + *top = config; + return 0; + } +} + +void snd_card_iterator_init(struct snd_card_iterator *iter, int cardno) +{ + iter->card = cardno; + iter->single = cardno >= 0; + iter->first = true; + iter->name[0] = '\0'; +} + +int snd_card_iterator_sinit(struct snd_card_iterator *iter, const char *cardname) +{ + int cardno = -1; + + if (cardname) { + if (strncmp(cardname, "hw:", 3) == 0) + cardname += 3; + cardno = snd_card_get_index(cardname); + if (cardno < 0) { + error("Cannot find soundcard '%s'...", cardname); + return cardno; + } + } + snd_card_iterator_init(iter, cardno); + return 0; +} + +const char *snd_card_iterator_next(struct snd_card_iterator *iter) +{ + if (iter->single) { + if (iter->first) { + iter->first = false; + goto retval; + } + return NULL; + } + if (snd_card_next(&iter->card) < 0) { + if (!ignore_nocards && iter->first) + error("No soundcards found..."); + return NULL; + } + iter->first = false; + if (iter->card < 0) + return NULL; +retval: + snprintf(iter->name, sizeof(iter->name), "hw:%d", iter->card); + + return (const char *)iter->name; +} + +int snd_card_iterator_error(struct snd_card_iterator *iter) +{ + return iter->first ? (ignore_nocards ? 0 : -ENODEV) : 0; +} + +static int cleanup_filename_filter(const struct dirent *dirent) +{ + size_t flen; + + if (dirent == NULL) + return 0; + if (dirent->d_type == DT_DIR) + return 0; + + flen = strlen(dirent->d_name); + if (flen <= 5) + return 0; + + if (strncmp(&dirent->d_name[flen-5], ".conf", 5) == 0) + return 1; + + return 0; +} + +int snd_card_clean_cfgdir(const char *cfgdir, int cardno) +{ + char path[PATH_MAX]; + struct dirent **list; + int lasterr = 0, n, j; + + snprintf(path, sizeof(path), "%s/card%d.conf.d", cfgdir, cardno); + n = scandir(path, &list, cleanup_filename_filter, NULL); + if (n < 0) { + if (errno == ENOENT) + return 0; + return -errno; + } + for (j = 0; j < n; j++) { + snprintf(path, sizeof(path), "%s/card%d.conf.d/%s", cfgdir, cardno, list[j]->d_name); + if (remove(path)) { + error("Unable to remove file '%s'", path); + lasterr = -errno; + } + } + + return lasterr; +} diff --git a/alsaloop/Makefile.am b/alsaloop/Makefile.am new file mode 100644 index 0000000..4cdf20b --- /dev/null +++ b/alsaloop/Makefile.am @@ -0,0 +1,14 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +LDADD = -lm +AM_CFLAGS = -D_GNU_SOURCE +if HAVE_SAMPLERATE +LDADD += -lsamplerate +endif +# LDFLAGS = -static +# CFLAGS += -g -Wall + +bin_PROGRAMS = alsaloop +alsaloop_SOURCES = alsaloop.c pcmjob.c control.c +noinst_HEADERS = alsaloop.h +man_MANS = alsaloop.1 +EXTRA_DIST = alsaloop.1 diff --git a/alsaloop/alsaloop.1 b/alsaloop/alsaloop.1 new file mode 100644 index 0000000..006494d --- /dev/null +++ b/alsaloop/alsaloop.1 @@ -0,0 +1,213 @@ +.TH ALSALOOP 1 "5 Aug 2010" +.SH NAME +alsaloop \- command\-line PCM loopback +.SH SYNOPSIS +\fBalsaloop\fP [\fI\-option\fP] [\fIcmd\fP] +.SH DESCRIPTION + +\fBalsaloop\fP allows create a PCM loopback between a PCM capture device +and a PCM playback device. + +\fBalsaloop\fP supports multiple soundcards, adaptive clock synchronization, +adaptive rate resampling using the samplerate library (if available in +the system). Also, mixer controls can be redirected from one card to +another (for example Master and PCM). + +.SH OPTIONS + +.TP +\fI\-h\fP | \fI\-\-help\fP + +Prints the help information. + +.TP +\fI\-g \fP | \fI\-\-config=\fP + +Use given configuration file. The syntax of this file is simple: one line +contains the command line options for one job. The '#' means comment and +rest of line is ignored. Example: + + # First line \- comment, second line \- first job + \-C hw:1,0 \-P hw:0,0 \-t 50000 \-T 1 + # Third line \- comment, fourth line \- second job + \-C hw:1,1 \-P hw:0,1 \-t 40000 \-T 2 + +.TP +\fI\-d\fP | \fI\-\-daemonize\fP + +Daemonize the main process and use syslog for messages. + +.TP +\fI\-P \fP | \fI\-\-pdevice=\fP + +Use given playback device. + +.TP +\fI\-C \fP | \fI\-\-cdevice=\fP + +Use given capture device. + +.TP +\fI\-X \fP | \fI\-\-pctl=\fP + +Use given CTL device for playback. + +.TP +\fI\-Y \fP | \fI\-\-cctl=\fP + +Use given CTL device for capture. + +.TP +\fI\-x \fP | \fI\-\-prateshift=\fP + +Specify ctl ascii name for playshift sync mode (see the Examples section). + +.TP +\fI\-l \fP | \fI\-\-latency=\fP + +Requested latency in frames. + +.TP +\fI\-t \fP | \fI\-\-tlatency=\fP + +Requested latency in usec (1/1000000sec). + +.TP +\fI\-f \fP | \fI\-\-format=\fP + +Format specification (usually S16_LE S32_LE). Use \-h to list all formats. +Default format is S16_LE. + +.TP +\fI\-c \fP | \fI\-\-channels=\fP + +Channel count specification. Default value is 2. + +.TP +\fI\-r \fP | \fI\-\-rate=\fP + +Rate specification. Default value is 48000 (Hz). + +.TP +\fI\-n\fP | \fI\-\-resample\fP + +Allow rate resampling using alsa\-lib. + +.TP +\fI\-A \fP | \fI\-\-samplerate=\fP + +Use libsamplerate and choose a converter: + + 0 or sincbest \- best quality + 1 or sincmedium \- medium quality + 2 or sincfastest \- lowest quality + 3 or zerohold \- hold zero samples + 4 or linear \- worst quality - linear resampling + 5 or auto \- choose best method + +.TP +\fI\-B \fP | \fI\-\-buffer=\fP + +Buffer size in frames. + +.TP +\fI\-E \fP | \fI\-\-period=\fP + +Period size in frames. + +.TP +\fI\-s \fP | \fI\-\-seconds=\fP + +Duration of loop in seconds. + +.TP +\fI\-b\fP | \fI\-\-nblock\fP + +Non\-block mode (very early process wakeup). Eats more CPU. + +.TP +\fI\-S \fP | \fI\-\-sync=\fP + +Sync mode specification for capture to playback stream: + 0 or none \- do not touch the stream + 1 or simple \- add or remove samples to keep + both streams synchronized + 2 or captshift \- use driver for the capture device + (if supported) to compensate + the rate shift + 3 or playshift \- use driver for the playback device + (if supported) to compensate + the rate shift + 4 or samplerate \- use samplerate library to do rate resampling + 5 or auto \- automatically selects the best method + in this order: captshift, playshift, + samplerate, simple + +.TP +\fI\-T \fP | \fI\-\-thread=\fP + +Thread number (\-1 means create a unique thread). All jobs with same +thread numbers are run within one thread. + +.TP +\fI\-m \fP | \fI\-\-mixer=\fP + +Redirect mixer control from the playback card to the capture card. Format of +\fImixid\fP is SRCID(PLAYBACK)[@DSTID(PLAYBACK)]: + + "name='Master Playback Switch'@name='Another Switch'" + "name='PCM Playback Volume'" + +Known attributes: + + name \- control ID name + index \- control ID index + device \- control ID device + subdevice \- control ID subdevice + iface \- control ID interface + numid \- control ID numid + +.TP +\fI\-O \fP | \fI\-\-ossmixer=\fP + +Redirect mixer control from the OSS Mixer emulation layer (capture card) +to the ALSA layer (capture card). Format of \fIossmixid\fP is +ALSAID[,INDEX]@OSSID: + + "Master@VOLUME" + "PCM,1@ALTPCM" + +Known OSS attributes: + + VOLUME, BASS, TREBLE, SYNTH, PCM, SPEAKER, LINE, MIC, CD, IMIX, ALTPCM, + RECLEV, IGAIN, OGAIN, LINE1, LINE2, LINE3, DIGITAL1, DIGITAL2, DIGITAL3, + PHONEIN, PHONEOUT, VIDEO, RADIO, MONITOR + +.TP +\fI\-v\fP | \fI\-\-verbose\fP + +Verbose mode. Use multiple times to increase verbosity. + + +.TP +\fI\-U\fP | \fI\-\-xrun\fP + +Verbose xrun profiling. + +.TP +\fI\-W \fP | \fI\-\-wake=\fP + +Set process wake timeout. + +.SH EXAMPLES +.nf +\fBalsaloop \-C hw:0,0 \-P hw:1,0 \-t 50000\fR + +\fBalsaloop \-C hw:soundcard,0 \-P hw:Loopback,0 \-t 50000 \-S playshift \\ +\-x iface=PCM,name='PCM Rate Shift 100000',device=1\fR +.ne +.SH BUGS +None known. +.SH AUTHOR +\fBalsaloop\fP is by Jaroslav Kysela . +This document is by Jaroslav Kysela . diff --git a/alsaloop/alsaloop.c b/alsaloop/alsaloop.c new file mode 100644 index 0000000..51fb646 --- /dev/null +++ b/alsaloop/alsaloop.c @@ -0,0 +1,952 @@ +/* + * A simple PCM loopback utility with adaptive sample rate support + * + * Author: Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "alsaloop.h" +#include "os_compat.h" + +struct loopback_thread { + int threaded; + pthread_t thread; + int exitcode; + struct loopback **loopbacks; + int loopbacks_count; + snd_output_t *output; +}; + +int quit = 0; +int verbose = 0; +int workarounds = 0; +int daemonize = 0; +int use_syslog = 0; +struct loopback **loopbacks = NULL; +int loopbacks_count = 0; +char **my_argv = NULL; +int my_argc = 0; +struct loopback_thread *threads; +int threads_count = 0; +pthread_t main_job; +int arg_default_xrun = 0; +int arg_default_wake = 0; + +static void my_exit(struct loopback_thread *thread, int exitcode) +{ + int i; + + for (i = 0; i < thread->loopbacks_count; i++) + pcmjob_done(thread->loopbacks[i]); + if (thread->threaded) { + thread->exitcode = exitcode; + pthread_exit(0); + } + exit(exitcode); +} + +static int create_loopback_handle(struct loopback_handle **_handle, + const char *device, + const char *ctldev, + const char *id) +{ + char idbuf[1024]; + struct loopback_handle *handle; + + handle = calloc(1, sizeof(*handle)); + if (handle == NULL) + return -ENOMEM; + if (device == NULL) + device = "hw:0,0"; + handle->device = strdup(device); + if (handle->device == NULL) { + free(handle); + return -ENOMEM; + } + if (ctldev) { + handle->ctldev = strdup(ctldev); + if (handle->ctldev == NULL) { + free(handle->device); + free(handle); + return -ENOMEM; + } + } else { + handle->ctldev = NULL; + } + snprintf(idbuf, sizeof(idbuf)-1, "%s %s", id, device); + idbuf[sizeof(idbuf)-1] = '\0'; + handle->id = strdup(idbuf); + handle->access = SND_PCM_ACCESS_RW_INTERLEAVED; + handle->format = SND_PCM_FORMAT_S16_LE; + handle->rate = handle->rate_req = 48000; + handle->channels = 2; + handle->resample = 0; + *_handle = handle; + return 0; +} + +static int create_loopback(struct loopback **_handle, + struct loopback_handle *play, + struct loopback_handle *capt, + snd_output_t *output) +{ + struct loopback *handle; + + handle = calloc(1, sizeof(*handle)); + if (handle == NULL) + return -ENOMEM; + handle->play = play; + handle->capt = capt; + play->loopback = handle; + capt->loopback = handle; + handle->latency_req = 0; + handle->latency_reqtime = 10000; + handle->loop_time = ~0UL; + handle->loop_limit = ~0ULL; + handle->output = output; + handle->state = output; +#ifdef USE_SAMPLERATE + handle->src_enable = 1; + handle->src_converter_type = SRC_SINC_BEST_QUALITY; +#endif + *_handle = handle; + return 0; +} + +static void set_loop_time(struct loopback *loop, unsigned long loop_time) +{ + loop->loop_time = loop_time; + loop->loop_limit = loop->capt->rate * loop_time; +} + +static void setscheduler(void) +{ + struct sched_param sched_param; + + if (sched_getparam(0, &sched_param) < 0) { + logit(LOG_WARNING, "Scheduler getparam failed.\n"); + return; + } + sched_param.sched_priority = sched_get_priority_max(SCHED_RR); + if (!sched_setscheduler(0, SCHED_RR, &sched_param)) { + if (verbose) + logit(LOG_WARNING, "Scheduler set to Round Robin with priority %i\n", sched_param.sched_priority); + return; + } + if (verbose) + logit(LOG_INFO, "!!!Scheduler set to Round Robin with priority %i FAILED!\n", sched_param.sched_priority); +} + +void help(void) +{ + int k; + printf( +"Usage: alsaloop [OPTION]...\n\n" +"-h,--help help\n" +"-g,--config configuration file (one line = one job specified)\n" +"-d,--daemonize daemonize the main process and use syslog for errors\n" +"-P,--pdevice playback device\n" +"-C,--cdevice capture device\n" +"-X,--pctl playback ctl device\n" +"-Y,--cctl capture ctl device\n" +"-x,--prateshift playback 'PCM Rate Shift 100000' ascii ctl name\n" +"-l,--latency requested latency in frames\n" +"-t,--tlatency requested latency in usec (1/1000000sec)\n" +"-f,--format sample format\n" +"-c,--channels channels\n" +"-r,--rate rate\n" +"-n,--resample resample in alsa-lib\n" +"-A,--samplerate use converter (0=sincbest,1=sincmedium,2=sincfastest,\n" +" 3=zerohold,4=linear)\n" +"-B,--buffer buffer size in frames\n" +"-E,--period period size in frames\n" +"-s,--seconds duration of loop in seconds\n" +"-b,--nblock non-block mode (very early process wakeup)\n" +"-S,--sync sync mode(0=none,1=simple,2=captshift,3=playshift,4=samplerate,\n" +" 5=auto)\n" +"-a,--slave stream parameters slave mode (0=auto, 1=on, 2=off)\n" +"-T,--thread thread number (-1 = create unique)\n" +"-m,--mixer redirect mixer, argument is:\n" +" SRC_SLAVE_ID(PLAYBACK)[@DST_SLAVE_ID(CAPTURE)]\n" +"-O,--ossmixer rescan and redirect oss mixer, argument is:\n" +" ALSA_ID@OSS_ID (for example: \"Master@VOLUME\")\n" +"-e,--effect apply an effect (bandpass filter sweep)\n" +"-v,--verbose verbose mode (more -v means more verbose)\n" +"-w,--workaround use workaround (serialopen)\n" +"-U,--xrun xrun profiling\n" +"-W,--wake process wake timeout in ms\n" +"-z,--syslog use syslog for errors\n" +); + printf("\nRecognized sample formats are:"); + for (k = 0; k < SND_PCM_FORMAT_LAST; ++k) { + const char *s = snd_pcm_format_name(k); + if (s) + printf(" %s", s); + } + printf("\n\n"); + printf( +"Tip #1 (usable 500ms latency, good CPU usage, superb xrun prevention):\n" +" alsaloop -t 500000\n" +"Tip #2 (superb 1ms latency, but heavy CPU usage):\n" +" alsaloop -t 1000\n" +); +} + +static long timediff(struct timeval t1, struct timeval t2) +{ + signed long l; + + t1.tv_sec -= t2.tv_sec; + l = (signed long) t1.tv_usec - (signed long) t2.tv_usec; + if (l < 0) { + t1.tv_sec--; + l = 1000000 + l; + l %= 1000000; + } + return (t1.tv_sec * 1000000) + l; +} + +static void add_loop(struct loopback *loop) +{ + loopbacks = realloc(loopbacks, (loopbacks_count + 1) * + sizeof(struct loopback *)); + if (loopbacks == NULL) { + logit(LOG_CRIT, "No enough memory\n"); + exit(EXIT_FAILURE); + } + loopbacks[loopbacks_count++] = loop; +} + +static int init_mixer_control(struct loopback_control *control, + char *id) +{ + int err; + + err = snd_ctl_elem_id_malloc(&control->id); + if (err < 0) + return err; + err = snd_ctl_elem_info_malloc(&control->info); + if (err < 0) + return err; + err = snd_ctl_elem_value_malloc(&control->value); + if (err < 0) + return err; + err = control_parse_id(id, control->id); + if (err < 0) + return err; + return 0; +} + +static int add_mixers(struct loopback *loop, + char **mixers, + int mixers_count) +{ + struct loopback_mixer *mixer, *last = NULL; + char *str1; + int err; + + while (mixers_count > 0) { + mixer = calloc(1, sizeof(*mixer)); + if (mixer == NULL) + return -ENOMEM; + if (last) + last->next = mixer; + else + loop->controls = mixer; + last = mixer; + str1 = strchr(*mixers, '@'); + if (str1) + *str1 = '\0'; + err = init_mixer_control(&mixer->src, *mixers); + if (err < 0) { + logit(LOG_CRIT, "Wrong mixer control ID syntax '%s'\n", *mixers); + return -EINVAL; + } + err = init_mixer_control(&mixer->dst, str1 ? str1 + 1 : *mixers); + if (err < 0) { + logit(LOG_CRIT, "Wrong mixer control ID syntax '%s'\n", str1 ? str1 + 1 : *mixers); + return -EINVAL; + } + if (str1) + *str1 = '@'; + mixers++; + mixers_count--; + } + return 0; +} + +static int add_oss_mixers(struct loopback *loop, + char **mixers, + int mixers_count) +{ + struct loopback_ossmixer *mixer, *last = NULL; + char *str1, *str2; + + while (mixers_count > 0) { + mixer = calloc(1, sizeof(*mixer)); + if (mixer == NULL) + return -ENOMEM; + if (last) + last->next = mixer; + else + loop->oss_controls = mixer; + last = mixer; + str1 = strchr(*mixers, ','); + if (str1) + *str1 = '\0'; + str2 = strchr(str1 ? str1 + 1 : *mixers, '@'); + if (str2) + *str2 = '\0'; + mixer->alsa_id = strdup(*mixers); + if (str1) + mixer->alsa_index = atoi(str1); + mixer->oss_id = strdup(str2 ? str2 + 1 : *mixers); + if (mixer->alsa_id == NULL || mixer->oss_id == NULL) { + logit(LOG_CRIT, "Not enough memory"); + return -ENOMEM; + } + if (str1) + *str1 = ','; + if (str2) + *str2 = ','; + mixers++; + mixers_count--; + } + return 0; +} + +static void enable_syslog(void) +{ + if (!use_syslog) { + use_syslog = 1; + openlog("alsaloop", LOG_NDELAY|LOG_PID, LOG_DAEMON); + } +} + +static int parse_config_file(const char *file, snd_output_t *output); + +static int parse_config(int argc, char *argv[], snd_output_t *output, + int cmdline) +{ + struct option long_option[] = + { + {"help", 0, NULL, 'h'}, + {"config", 1, NULL, 'g'}, + {"daemonize", 0, NULL, 'd'}, + {"pdevice", 1, NULL, 'P'}, + {"cdevice", 1, NULL, 'C'}, + {"pctl", 1, NULL, 'X'}, + {"cctl", 1, NULL, 'Y'}, + {"prateshift", 1, NULL, 'x'}, + {"latency", 1, NULL, 'l'}, + {"tlatency", 1, NULL, 't'}, + {"format", 1, NULL, 'f'}, + {"channels", 1, NULL, 'c'}, + {"rate", 1, NULL, 'r'}, + {"buffer", 1, NULL, 'B'}, + {"period", 1, NULL, 'E'}, + {"seconds", 1, NULL, 's'}, + {"nblock", 0, NULL, 'b'}, + {"effect", 0, NULL, 'e'}, + {"verbose", 0, NULL, 'v'}, + {"resample", 0, NULL, 'n'}, + {"samplerate", 1, NULL, 'A'}, + {"sync", 1, NULL, 'S'}, + {"slave", 1, NULL, 'a'}, + {"thread", 1, NULL, 'T'}, + {"mixer", 1, NULL, 'm'}, + {"ossmixer", 1, NULL, 'O'}, + {"workaround", 1, NULL, 'w'}, + {"xrun", 0, NULL, 'U'}, + {"syslog", 0, NULL, 'z'}, + {NULL, 0, NULL, 0}, + }; + int err, morehelp; + char *arg_config = NULL; + char *arg_pdevice = NULL; + char *arg_cdevice = NULL; + char *arg_pctl = NULL; + char *arg_cctl = NULL; + char *arg_prateshift = NULL; + unsigned int arg_latency_req = 0; + unsigned int arg_latency_reqtime = 10000; + snd_pcm_format_t arg_format = SND_PCM_FORMAT_S16_LE; + unsigned int arg_channels = 2; + unsigned int arg_rate = 48000; + snd_pcm_uframes_t arg_buffer_size = 0; + snd_pcm_uframes_t arg_period_size = 0; + unsigned long arg_loop_time = ~0UL; + int arg_nblock = 0; + // int arg_effect = 0; + int arg_resample = 0; +#ifdef USE_SAMPLERATE + int arg_samplerate = SRC_SINC_FASTEST + 1; +#endif + int arg_sync = SYNC_TYPE_AUTO; + int arg_slave = SLAVE_TYPE_AUTO; + int arg_thread = 0; + struct loopback *loop = NULL; + char *arg_mixers[MAX_MIXERS]; + int arg_mixers_count = 0; + char *arg_ossmixers[MAX_MIXERS]; + int arg_ossmixers_count = 0; + int arg_xrun = arg_default_xrun; + int arg_wake = arg_default_wake; + + morehelp = 0; + while (1) { + int c; + if ((c = getopt_long(argc, argv, + "hdg:P:C:X:Y:x:l:t:F:f:c:r:s:benvA:S:a:m:T:O:w:UW:z", + long_option, NULL)) < 0) + break; + switch (c) { + case 'h': + morehelp++; + break; + case 'g': + arg_config = strdup(optarg); + break; + case 'd': + daemonize = 1; + enable_syslog(); + break; + case 'P': + arg_pdevice = strdup(optarg); + break; + case 'C': + arg_cdevice = strdup(optarg); + break; + case 'X': + arg_pctl = strdup(optarg); + break; + case 'Y': + arg_cctl = strdup(optarg); + break; + case 'x': + arg_prateshift = strdup(optarg); + break; + case 'l': + err = atoi(optarg); + arg_latency_req = err >= 4 ? err : 4; + break; + case 't': + err = atoi(optarg); + arg_latency_reqtime = err >= 500 ? err : 500; + break; + case 'f': + arg_format = snd_pcm_format_value(optarg); + if (arg_format == SND_PCM_FORMAT_UNKNOWN) { + logit(LOG_WARNING, "Unknown format, setting to default S16_LE\n"); + arg_format = SND_PCM_FORMAT_S16_LE; + } + break; + case 'c': + err = atoi(optarg); + arg_channels = err >= 1 && err < 1024 ? err : 1; + break; + case 'r': + err = atoi(optarg); + arg_rate = err >= 4000 && err < 200000 ? err : 44100; + break; + case 'B': + err = atoi(optarg); + arg_buffer_size = err >= 32 && err < 200000 ? err : 0; + break; + case 'E': + err = atoi(optarg); + arg_period_size = err >= 32 && err < 200000 ? err : 0; + break; + case 's': + err = atoi(optarg); + arg_loop_time = err >= 1 && err <= 100000 ? err : 30; + break; + case 'b': + arg_nblock = 1; + break; + case 'e': + // arg_effect = 1; + break; + case 'n': + arg_resample = 1; + break; +#ifdef USE_SAMPLERATE + case 'A': + if (strcasecmp(optarg, "sincbest") == 0) + arg_samplerate = SRC_SINC_BEST_QUALITY; + else if (strcasecmp(optarg, "sincmedium") == 0) + arg_samplerate = SRC_SINC_MEDIUM_QUALITY; + else if (strcasecmp(optarg, "sincfastest") == 0) + arg_samplerate = SRC_SINC_FASTEST; + else if (strcasecmp(optarg, "zerohold") == 0) + arg_samplerate = SRC_ZERO_ORDER_HOLD; + else if (strcasecmp(optarg, "linear") == 0) + arg_samplerate = SRC_LINEAR; + else + arg_samplerate = atoi(optarg); + if (arg_samplerate < 0 || arg_samplerate > SRC_LINEAR) + arg_sync = SRC_SINC_FASTEST; + arg_samplerate += 1; + break; +#endif + case 'S': + if (strcasecmp(optarg, "samplerate") == 0) + arg_sync = SYNC_TYPE_SAMPLERATE; + else if (optarg[0] == 'n') + arg_sync = SYNC_TYPE_NONE; + else if (optarg[0] == 's') + arg_sync = SYNC_TYPE_SIMPLE; + else if (optarg[0] == 'c') + arg_sync = SYNC_TYPE_CAPTRATESHIFT; + else if (optarg[0] == 'p') + arg_sync = SYNC_TYPE_PLAYRATESHIFT; + else if (optarg[0] == 'r') + arg_sync = SYNC_TYPE_SAMPLERATE; + else if (optarg[0] == 'a') + arg_sync = SYNC_TYPE_AUTO; + else + arg_sync = atoi(optarg); + if (arg_sync < 0 || arg_sync > SYNC_TYPE_LAST) + arg_sync = SYNC_TYPE_AUTO; + break; + case 'a': + if (optarg[0] == 'a') + arg_slave = SLAVE_TYPE_AUTO; + else if (strcasecmp(optarg, "on") == 0) + arg_slave = SLAVE_TYPE_ON; + else if (strcasecmp(optarg, "off") == 0) + arg_slave = SLAVE_TYPE_OFF; + else + arg_slave = atoi(optarg); + if (arg_slave < 0 || arg_slave > SLAVE_TYPE_LAST) + arg_slave = SLAVE_TYPE_AUTO; + break; + case 'T': + arg_thread = atoi(optarg); + if (arg_thread < 0) + arg_thread = 10000000 + loopbacks_count; + break; + case 'm': + if (arg_mixers_count >= MAX_MIXERS) { + logit(LOG_CRIT, "Maximum redirected mixer controls reached (max %i)\n", (int)MAX_MIXERS); + exit(EXIT_FAILURE); + } + arg_mixers[arg_mixers_count++] = optarg; + break; + case 'O': + if (arg_ossmixers_count >= MAX_MIXERS) { + logit(LOG_CRIT, "Maximum redirected mixer controls reached (max %i)\n", (int)MAX_MIXERS); + exit(EXIT_FAILURE); + } + arg_ossmixers[arg_ossmixers_count++] = optarg; + break; + case 'v': + verbose++; + break; + case 'w': + if (strcasecmp(optarg, "serialopen") == 0) + workarounds |= WORKAROUND_SERIALOPEN; + break; + case 'U': + arg_xrun = 1; + if (cmdline) + arg_default_xrun = 1; + break; + case 'W': + arg_wake = atoi(optarg); + if (cmdline) + arg_default_wake = arg_wake; + break; + case 'z': + enable_syslog(); + break; + } + } + + if (morehelp) { + help(); + exit(EXIT_SUCCESS); + } + if (arg_config == NULL) { + struct loopback_handle *play; + struct loopback_handle *capt; + err = create_loopback_handle(&play, arg_pdevice, arg_pctl, "playback"); + if (err < 0) { + logit(LOG_CRIT, "Unable to create playback handle.\n"); + exit(EXIT_FAILURE); + } + err = create_loopback_handle(&capt, arg_cdevice, arg_cctl, "capture"); + if (err < 0) { + logit(LOG_CRIT, "Unable to create capture handle.\n"); + exit(EXIT_FAILURE); + } + err = create_loopback(&loop, play, capt, output); + if (err < 0) { + logit(LOG_CRIT, "Unable to create loopback handle.\n"); + exit(EXIT_FAILURE); + } + play->format = capt->format = arg_format; + play->rate = play->rate_req = capt->rate = capt->rate_req = arg_rate; + play->channels = capt->channels = arg_channels; + play->buffer_size_req = capt->buffer_size_req = arg_buffer_size; + play->period_size_req = capt->period_size_req = arg_period_size; + play->resample = capt->resample = arg_resample; + play->nblock = capt->nblock = arg_nblock ? 1 : 0; + loop->latency_req = arg_latency_req; + loop->latency_reqtime = arg_latency_reqtime; + loop->sync = arg_sync; + loop->slave = arg_slave; + loop->thread = arg_thread; + loop->xrun = arg_xrun; + loop->wake = arg_wake; + err = add_mixers(loop, arg_mixers, arg_mixers_count); + if (err < 0) { + logit(LOG_CRIT, "Unable to add mixer controls.\n"); + exit(EXIT_FAILURE); + } + err = add_oss_mixers(loop, arg_ossmixers, arg_ossmixers_count); + if (err < 0) { + logit(LOG_CRIT, "Unable to add ossmixer controls.\n"); + exit(EXIT_FAILURE); + } + if (arg_prateshift) + play->prateshift_name = arg_prateshift; + +#ifdef USE_SAMPLERATE + loop->src_enable = arg_samplerate > 0; + if (loop->src_enable) + loop->src_converter_type = arg_samplerate - 1; +#endif + set_loop_time(loop, arg_loop_time); + add_loop(loop); + return 0; + } + + return parse_config_file(arg_config, output); +} + +static int parse_config_file(const char *file, snd_output_t *output) +{ + FILE *fp; + char line[2048], word[2048]; + char *str, *ptr; + int argc, c, err = 0; + char **argv; + + fp = fopen(file, "r"); + if (fp == NULL) { + logit(LOG_CRIT, "Unable to open file '%s': %s\n", file, strerror(errno)); + return -EIO; + } + while (!feof(fp)) { + if (fgets(line, sizeof(line)-1, fp) == NULL) + break; + line[sizeof(line)-1] = '\0'; + my_argv = realloc(my_argv, my_argc + MAX_ARGS * sizeof(char *)); + if (my_argv == NULL) + return -ENOMEM; + argv = my_argv + my_argc; + argc = 0; + argv[argc++] = strdup(""); + my_argc++; + str = line; + while (*str) { + ptr = word; + while (*str && (*str == ' ' || *str < ' ')) + str++; + if (*str == '#') + goto __next; + if (*str == '\'' || *str == '\"') { + c = *str++; + while (*str && *str != c) + *ptr++ = *str++; + if (*str == c) + str++; + } else { + while (*str && *str != ' ' && *str != '\t') + *ptr++ = *str++; + } + if (ptr != word) { + if (*(ptr-1) == '\n') + ptr--; + *ptr = '\0'; + if (argc >= MAX_ARGS) { + logit(LOG_CRIT, "Too many arguments."); + goto __error; + } + argv[argc++] = strdup(word); + my_argc++; + } + } + /* erase runtime variables for getopt */ + optarg = NULL; + optind = opterr = 1; + optopt = '?'; + + err = parse_config(argc, argv, output, 0); + __next: + if (err < 0) + break; + err = 0; + } + __error: + fclose(fp); + + return err; +} + +static void thread_job1(void *_data) +{ + struct loopback_thread *thread = _data; + snd_output_t *output = thread->output; + struct pollfd *pfds = NULL; + int pfds_count = 0; + int i, j, err, wake = 1000000; + + setscheduler(); + + for (i = 0; i < thread->loopbacks_count; i++) { + err = pcmjob_init(thread->loopbacks[i]); + if (err < 0) { + logit(LOG_CRIT, "Loopback initialization failure.\n"); + my_exit(thread, EXIT_FAILURE); + } + } + for (i = 0; i < thread->loopbacks_count; i++) { + err = pcmjob_start(thread->loopbacks[i]); + if (err < 0) { + logit(LOG_CRIT, "Loopback start failure.\n"); + my_exit(thread, EXIT_FAILURE); + } + pfds_count += thread->loopbacks[i]->pollfd_count; + j = thread->loopbacks[i]->wake; + if (j > 0 && j < wake) + wake = j; + } + if (wake >= 1000000) + wake = -1; + pfds = calloc(pfds_count, sizeof(struct pollfd)); + if (pfds == NULL || pfds_count <= 0) { + logit(LOG_CRIT, "Poll FDs allocation failed.\n"); + my_exit(thread, EXIT_FAILURE); + } + while (!quit) { + struct timeval tv1, tv2; + for (i = j = 0; i < thread->loopbacks_count; i++) { + err = pcmjob_pollfds_init(thread->loopbacks[i], &pfds[j]); + if (err < 0) { + logit(LOG_CRIT, "Poll FD initialization failed.\n"); + my_exit(thread, EXIT_FAILURE); + } + j += err; + } + if (verbose > 10) + gettimeofday(&tv1, NULL); + err = poll(pfds, j, wake); + if (err < 0) + err = -errno; + if (verbose > 10) { + gettimeofday(&tv2, NULL); + snd_output_printf(output, "pool took %lius\n", timediff(tv2, tv1)); + } + if (err < 0) { + if (err == -EINTR || err == -ERESTART) + continue; + logit(LOG_CRIT, "Poll failed: %s\n", strerror(-err)); + my_exit(thread, EXIT_FAILURE); + } + for (i = j = 0; i < thread->loopbacks_count; i++) { + struct loopback *loop = thread->loopbacks[i]; + if (j < loop->active_pollfd_count) { + err = pcmjob_pollfds_handle(loop, &pfds[j]); + if (err < 0) { + logit(LOG_CRIT, "pcmjob failed.\n"); + exit(EXIT_FAILURE); + } + } + j += loop->active_pollfd_count; + } + } + + my_exit(thread, EXIT_SUCCESS); +} + +static void thread_job(struct loopback_thread *thread) +{ + if (!thread->threaded) { + thread_job1(thread); + return; + } + pthread_create(&thread->thread, NULL, (void *) &thread_job1, + (void *) thread); +} + +static void send_to_all(int sig) +{ + struct loopback_thread *thread; + int i; + + for (i = 0; i < threads_count; i++) { + thread = &threads[i]; + if (thread->threaded) + pthread_kill(thread->thread, sig); + } +} + +static void signal_handler(int sig ATTRIBUTE_UNUSED) +{ + quit = 1; + send_to_all(SIGUSR2); +} + +static void signal_handler_state(int sig) +{ + pthread_t self = pthread_self(); + struct loopback_thread *thread; + int i, j; + + if (pthread_equal(main_job, self)) + send_to_all(SIGUSR1); + for (i = 0; i < threads_count; i++) { + thread = &threads[i]; + if (thread->thread == self) { + for (j = 0; j < thread->loopbacks_count; j++) + pcmjob_state(thread->loopbacks[j]); + } + } + signal(sig, signal_handler_state); +} + +static void signal_handler_ignore(int sig) +{ + signal(sig, signal_handler_ignore); +} + +int main(int argc, char *argv[]) +{ + snd_output_t *output; + int i, j, k, l, err; + + err = snd_output_stdio_attach(&output, stdout, 0); + if (err < 0) { + logit(LOG_CRIT, "Output failed: %s\n", snd_strerror(err)); + exit(EXIT_FAILURE); + } + err = parse_config(argc, argv, output, 1); + if (err < 0) { + logit(LOG_CRIT, "Unable to parse arguments or configuration...\n"); + exit(EXIT_FAILURE); + } + while (my_argc > 0) + free(my_argv[--my_argc]); + free(my_argv); + + if (loopbacks_count <= 0) { + logit(LOG_CRIT, "No loopback defined...\n"); + exit(EXIT_FAILURE); + } + + if (daemonize) { + if (daemon(0, 0) < 0) { + logit(LOG_CRIT, "daemon() failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + i = fork(); + if (i < 0) { + logit(LOG_CRIT, "fork() failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + if (i > 0) { + /* wait(&i); */ + exit(EXIT_SUCCESS); + } + } + + /* we must sort thread IDs */ + j = -1; + do { + k = 0x7fffffff; + for (i = 0; i < loopbacks_count; i++) { + if (loopbacks[i]->thread < k && + loopbacks[i]->thread > j) + k = loopbacks[i]->thread; + } + j++; + for (i = 0; i < loopbacks_count; i++) { + if (loopbacks[i]->thread == k) + loopbacks[i]->thread = j; + } + } while (k != 0x7fffffff); + /* fix maximum thread id */ + for (i = 0, j = -1; i < loopbacks_count; i++) { + if (loopbacks[i]->thread > j) + j = loopbacks[i]->thread; + } + j += 1; + threads = calloc(1, sizeof(struct loopback_thread) * j); + if (threads == NULL) { + logit(LOG_CRIT, "No enough memory\n"); + exit(EXIT_FAILURE); + } + /* sort all threads */ + for (k = 0; k < j; k++) { + for (i = l = 0; i < loopbacks_count; i++) + if (loopbacks[i]->thread == k) + l++; + threads[k].loopbacks = malloc(l * sizeof(struct loopback *)); + threads[k].loopbacks_count = l; + threads[k].output = output; + threads[k].threaded = j > 1; + for (i = l = 0; i < loopbacks_count; i++) + if (loopbacks[i]->thread == k) + threads[k].loopbacks[l++] = loopbacks[i]; + } + threads_count = j; + main_job = pthread_self(); + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + signal(SIGABRT, signal_handler); + signal(SIGUSR1, signal_handler_state); + signal(SIGUSR2, signal_handler_ignore); + + for (k = 0; k < threads_count; k++) + thread_job(&threads[k]); + + if (j > 1) { + for (k = 0; k < threads_count; k++) + pthread_join(threads[k].thread, NULL); + } + + if (use_syslog) + closelog(); + exit(EXIT_SUCCESS); +} diff --git a/alsaloop/alsaloop.h b/alsaloop/alsaloop.h new file mode 100644 index 0000000..a5539ef --- /dev/null +++ b/alsaloop/alsaloop.h @@ -0,0 +1,221 @@ +/* + * A simple PCM loopback utility + * Copyright (c) 2010 by Jaroslav Kysela + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#ifdef HAVE_SAMPLERATE_H +#define USE_SAMPLERATE +#include +#else +enum { + SRC_SINC_BEST_QUALITY = 0, + SRC_SINC_MEDIUM_QUALITY = 1, + SRC_SINC_FASTEST = 2, + SRC_ZERO_ORDER_HOLD = 3, + SRC_LINEAR = 4 +}; +#endif + +#define MAX_ARGS 128 +#define MAX_MIXERS 64 + +#if 0 +#define FILE_PWRITE "/tmp/alsaloop.praw" +#define FILE_CWRITE "/tmp/alsaloop.craw" +#endif + +#define WORKAROUND_SERIALOPEN (1<<0) + +typedef enum _sync_type { + SYNC_TYPE_NONE = 0, + SYNC_TYPE_SIMPLE, /* add or remove samples */ + SYNC_TYPE_CAPTRATESHIFT, + SYNC_TYPE_PLAYRATESHIFT, + SYNC_TYPE_SAMPLERATE, + SYNC_TYPE_AUTO, /* order: CAPTRATESHIFT, PLAYRATESHIFT, */ + /* SAMPLERATE, SIMPLE */ + SYNC_TYPE_LAST = SYNC_TYPE_AUTO +} sync_type_t; + +typedef enum _slave_type { + SLAVE_TYPE_AUTO = 0, + SLAVE_TYPE_ON = 1, + SLAVE_TYPE_OFF = 2, + SLAVE_TYPE_LAST = SLAVE_TYPE_OFF +} slave_type_t; + +struct loopback_control { + snd_ctl_elem_id_t *id; + snd_ctl_elem_info_t *info; + snd_ctl_elem_value_t *value; +}; + +struct loopback_mixer { + unsigned int skip:1; + struct loopback_control src; + struct loopback_control dst; + struct loopback_mixer *next; +}; + +struct loopback_ossmixer { + unsigned int skip:1; + const char *alsa_id; + int alsa_index; + const char *oss_id; + struct loopback_ossmixer *next; +}; + +struct loopback_handle { + struct loopback *loopback; + char *device; + char *ctldev; + char *id; + int card_number; + snd_pcm_t *handle; + snd_pcm_access_t access; + snd_pcm_format_t format; + unsigned int rate; + unsigned int rate_req; + unsigned int channels; + unsigned int buffer_size; + unsigned int period_size; + snd_pcm_uframes_t avail_min; + unsigned int buffer_size_req; + unsigned int period_size_req; + unsigned int frame_size; + unsigned int resample:1; /* do resample */ + unsigned int nblock:1; /* do block (period size) transfers */ + unsigned int xrun_pending:1; + unsigned int pollfd_count; + /* I/O job */ + char *buf; /* I/O buffer */ + snd_pcm_uframes_t buf_pos; /* I/O position */ + snd_pcm_uframes_t buf_count; /* filled samples */ + snd_pcm_uframes_t buf_size; /* buffer size in frames */ + snd_pcm_uframes_t buf_over; /* capture buffer overflow */ + int stall; + /* statistics */ + snd_pcm_uframes_t max; + unsigned long long counter; + unsigned long sync_point; /* in samples */ + snd_pcm_sframes_t last_delay; + double pitch; + snd_pcm_uframes_t total_queued; + /* control */ + snd_ctl_t *ctl; + unsigned int ctl_pollfd_count; + snd_ctl_elem_value_t *ctl_notify; + snd_ctl_elem_value_t *ctl_rate_shift; + snd_ctl_elem_value_t *ctl_pitch; + snd_ctl_elem_value_t *ctl_active; + snd_ctl_elem_value_t *ctl_format; + snd_ctl_elem_value_t *ctl_rate; + snd_ctl_elem_value_t *ctl_channels; + char *prateshift_name; /* ascii name for the playback rate shift ctl elem */ +}; + +struct loopback { + char *id; + struct loopback_handle *capt; + struct loopback_handle *play; + snd_pcm_uframes_t latency; /* final latency in frames */ + unsigned int latency_req; /* in frames */ + unsigned int latency_reqtime; /* in us */ + unsigned long loop_time; /* ~0 = unlimited (in seconds) */ + unsigned long long loop_limit; /* ~0 = unlimited (in frames) */ + snd_output_t *output; + snd_output_t *state; + int pollfd_count; + int active_pollfd_count; + unsigned int linked:1; /* linked streams */ + unsigned int reinit:1; + unsigned int running:1; + unsigned int stop_pending:1; + snd_pcm_uframes_t stop_count; + sync_type_t sync; /* type of sync */ + slave_type_t slave; + int thread; /* thread number */ + unsigned int wake; + /* statistics */ + double pitch; + double pitch_delta; + snd_pcm_sframes_t pitch_diff; + snd_pcm_sframes_t pitch_diff_min; + snd_pcm_sframes_t pitch_diff_max; + unsigned int total_queued_count; + snd_timestamp_t tstamp_start; + snd_timestamp_t tstamp_end; + /* xrun profiling */ + unsigned int xrun:1; /* xrun profiling */ + snd_timestamp_t xrun_last_update; + snd_timestamp_t xrun_last_wake0; + snd_timestamp_t xrun_last_wake; + snd_timestamp_t xrun_last_check0; + snd_timestamp_t xrun_last_check; + snd_pcm_sframes_t xrun_last_pdelay; + snd_pcm_sframes_t xrun_last_cdelay; + snd_pcm_uframes_t xrun_buf_pcount; + snd_pcm_uframes_t xrun_buf_ccount; + unsigned int xrun_out_frames; + long xrun_max_proctime; + double xrun_max_missing; + /* control mixer */ + struct loopback_mixer *controls; + struct loopback_ossmixer *oss_controls; + /* sample rate */ + unsigned int use_samplerate:1; +#ifdef USE_SAMPLERATE + unsigned int src_enable:1; + int src_converter_type; + SRC_STATE *src_state; + SRC_DATA src_data; + unsigned int src_out_frames; +#endif +#ifdef FILE_CWRITE + FILE *cfile; +#endif +#ifdef FILE_PWRITE + FILE *pfile; +#endif +}; + +extern int verbose; +extern int workarounds; +extern int use_syslog; + +#define logit(priority, fmt, args...) do { \ + if (use_syslog) \ + syslog(priority, fmt, ##args); \ + else \ + fprintf(stderr, fmt, ##args); \ +} while (0) + +int pcmjob_init(struct loopback *loop); +int pcmjob_done(struct loopback *loop); +int pcmjob_start(struct loopback *loop); +int pcmjob_stop(struct loopback *loop); +int pcmjob_pollfds_init(struct loopback *loop, struct pollfd *fds); +int pcmjob_pollfds_handle(struct loopback *loop, struct pollfd *fds); +void pcmjob_state(struct loopback *loop); + +int control_parse_id(const char *str, snd_ctl_elem_id_t *id); +int control_id_match(snd_ctl_elem_id_t *id1, snd_ctl_elem_id_t *id2); +int control_init(struct loopback *loop); +int control_done(struct loopback *loop); +int control_event(struct loopback_handle *lhandle, snd_ctl_event_t *ev); diff --git a/alsaloop/control.c b/alsaloop/control.c new file mode 100644 index 0000000..d3a7718 --- /dev/null +++ b/alsaloop/control.c @@ -0,0 +1,426 @@ +/* + * A simple PCM loopback utility + * Copyright (c) 2010 by Jaroslav Kysela + * + * Author: Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include "alsaloop.h" +#include "os_compat.h" + +static char *id_str(snd_ctl_elem_id_t *id) +{ + static char str[128]; + + sprintf(str, "%i,%s,%i,%i,%s,%i", + snd_ctl_elem_id_get_numid(id), + snd_ctl_elem_iface_name(snd_ctl_elem_id_get_interface(id)), + snd_ctl_elem_id_get_device(id), + snd_ctl_elem_id_get_subdevice(id), + snd_ctl_elem_id_get_name(id), + snd_ctl_elem_id_get_index(id)); + return str; +} + +int control_parse_id(const char *str, snd_ctl_elem_id_t *id) +{ + int c, size, numid; + char *ptr; + + while (*str == ' ' || *str == '\t') + str++; + if (!(*str)) + return -EINVAL; + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); /* default */ + while (*str) { + if (!strncasecmp(str, "numid=", 6)) { + str += 6; + numid = atoi(str); + if (numid <= 0) { + logit(LOG_CRIT, "Invalid numid %d\n", numid); + return -EINVAL; + } + snd_ctl_elem_id_set_numid(id, atoi(str)); + while (isdigit(*str)) + str++; + } else if (!strncasecmp(str, "iface=", 6)) { + str += 6; + if (!strncasecmp(str, "card", 4)) { + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD); + str += 4; + } else if (!strncasecmp(str, "mixer", 5)) { + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + str += 5; + } else if (!strncasecmp(str, "pcm", 3)) { + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM); + str += 3; + } else if (!strncasecmp(str, "rawmidi", 7)) { + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_RAWMIDI); + str += 7; + } else if (!strncasecmp(str, "timer", 5)) { + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_TIMER); + str += 5; + } else if (!strncasecmp(str, "sequencer", 9)) { + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_SEQUENCER); + str += 9; + } else { + return -EINVAL; + } + } else if (!strncasecmp(str, "name=", 5)) { + char buf[64]; + str += 5; + ptr = buf; + size = 0; + if (*str == '\'' || *str == '\"') { + c = *str++; + while (*str && *str != c) { + if (size < (int)sizeof(buf)) { + *ptr++ = *str; + size++; + } + str++; + } + if (*str == c) + str++; + } else { + while (*str && *str != ',') { + if (size < (int)sizeof(buf)) { + *ptr++ = *str; + size++; + } + str++; + } + } + *ptr = '\0'; + snd_ctl_elem_id_set_name(id, buf); + } else if (!strncasecmp(str, "index=", 6)) { + str += 6; + snd_ctl_elem_id_set_index(id, atoi(str)); + while (isdigit(*str)) + str++; + } else if (!strncasecmp(str, "device=", 7)) { + str += 7; + snd_ctl_elem_id_set_device(id, atoi(str)); + while (isdigit(*str)) + str++; + } else if (!strncasecmp(str, "subdevice=", 10)) { + str += 10; + snd_ctl_elem_id_set_subdevice(id, atoi(str)); + while (isdigit(*str)) + str++; + } + if (*str == ',') { + str++; + } else { + if (*str) + return -EINVAL; + } + } + return 0; +} + +int control_id_match(snd_ctl_elem_id_t *id1, snd_ctl_elem_id_t *id2) +{ + if (snd_ctl_elem_id_get_interface(id1) != + snd_ctl_elem_id_get_interface(id2)) + return 0; + if (snd_ctl_elem_id_get_device(id1) != + snd_ctl_elem_id_get_device(id2)) + return 0; + if (snd_ctl_elem_id_get_subdevice(id1) != + snd_ctl_elem_id_get_subdevice(id2)) + return 0; + if (strcmp(snd_ctl_elem_id_get_name(id1), + snd_ctl_elem_id_get_name(id2)) != 0) + return 0; + if (snd_ctl_elem_id_get_index(id1) != + snd_ctl_elem_id_get_index(id2)) + return 0; + return 1; +} + +static int control_init1(struct loopback_handle *lhandle, + struct loopback_control *ctl) +{ + int err; + + snd_ctl_elem_info_set_id(ctl->info, ctl->id); + snd_ctl_elem_value_set_id(ctl->value, ctl->id); + if (lhandle->ctl == NULL) { + logit(LOG_WARNING, "Unable to read control info for '%s'\n", id_str(ctl->id)); + return -EIO; + } + err = snd_ctl_elem_info(lhandle->ctl, ctl->info); + if (err < 0) { + logit(LOG_WARNING, "Unable to read control info '%s': %s\n", id_str(ctl->id), snd_strerror(err)); + return err; + } + err = snd_ctl_elem_read(lhandle->ctl, ctl->value); + if (err < 0) { + logit(LOG_WARNING, "Unable to read control value (init1) '%s': %s\n", id_str(ctl->id), snd_strerror(err)); + return err; + } + return 0; +} + +static int copy_value(struct loopback_control *dst, + struct loopback_control *src) +{ + snd_ctl_elem_type_t type; + unsigned int i, count; + + type = snd_ctl_elem_info_get_type(dst->info); + count = snd_ctl_elem_info_get_count(dst->info); + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + for (i = 0; i < count; i++) + snd_ctl_elem_value_set_boolean(dst->value, + i, snd_ctl_elem_value_get_boolean(src->value, i)); + break; + case SND_CTL_ELEM_TYPE_INTEGER: + for (i = 0; i < count; i++) { + snd_ctl_elem_value_set_integer(dst->value, + i, snd_ctl_elem_value_get_integer(src->value, i)); + } + break; + default: + logit(LOG_CRIT, "Unable to copy control value for type %s\n", snd_ctl_elem_type_name(type)); + return -EINVAL; + } + return 0; +} + +static int oss_set(struct loopback *loop, + struct loopback_ossmixer *ossmix, + int enable) +{ + char buf[128], file[128]; + int fd; + + if (loop->capt->card_number < 0) + return 0; + if (!enable) { + sprintf(buf, "%s \"\" 0\n", ossmix->oss_id); + } else { + sprintf(buf, "%s \"%s\" %i\n", ossmix->oss_id, ossmix->alsa_id, ossmix->alsa_index); + } + sprintf(file, "/proc/asound/card%i/oss_mixer", loop->capt->card_number); + if (verbose) + snd_output_printf(loop->output, "%s: Initialize OSS volume %s: %s", loop->id, file, buf); + fd = open(file, O_WRONLY); + if (fd >= 0 && write(fd, buf, strlen(buf)) == (ssize_t)strlen(buf)) { + close(fd); + return 0; + } + if (fd >= 0) + close(fd); + logit(LOG_INFO, "%s: Unable to initialize OSS Mixer ID '%s'\n", loop->id, ossmix->oss_id); + return -1; +} + +static int control_init2(struct loopback *loop, + struct loopback_mixer *mix) +{ + snd_ctl_elem_type_t type; + unsigned int count; + int err; + + snd_ctl_elem_info_copy(mix->dst.info, mix->src.info); + snd_ctl_elem_info_set_id(mix->dst.info, mix->dst.id); + snd_ctl_elem_value_clear(mix->dst.value); + snd_ctl_elem_value_set_id(mix->dst.value, mix->dst.id); + type = snd_ctl_elem_info_get_type(mix->dst.info); + count = snd_ctl_elem_info_get_count(mix->dst.info); + snd_ctl_elem_remove(loop->capt->ctl, mix->dst.id); + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + err = snd_ctl_elem_add_boolean(loop->capt->ctl, + mix->dst.id, count); + copy_value(&mix->dst, &mix->src); + break; + case SND_CTL_ELEM_TYPE_INTEGER: + err = snd_ctl_elem_add_integer(loop->capt->ctl, + mix->dst.id, count, + snd_ctl_elem_info_get_min(mix->dst.info), + snd_ctl_elem_info_get_max(mix->dst.info), + snd_ctl_elem_info_get_step(mix->dst.info)); + copy_value(&mix->dst, &mix->src); + break; + default: + logit(LOG_CRIT, "Unable to handle control type %s\n", snd_ctl_elem_type_name(type)); + err = -EINVAL; + break; + } + if (err < 0) { + logit(LOG_CRIT, "Unable to create control '%s': %s\n", id_str(mix->dst.id), snd_strerror(err)); + return err; + } + err = snd_ctl_elem_unlock(loop->capt->ctl, mix->dst.id); + if (err < 0) { + logit(LOG_CRIT, "Unable to unlock control info '%s': %s\n", id_str(mix->dst.id), snd_strerror(err)); + return err; + } + err = snd_ctl_elem_info(loop->capt->ctl, mix->dst.info); + if (err < 0) { + logit(LOG_CRIT, "Unable to read control info '%s': %s\n", id_str(mix->dst.id), snd_strerror(err)); + return err; + } + if (snd_ctl_elem_info_is_tlv_writable(mix->dst.info)) { + unsigned int tlv[64]; + err = snd_ctl_elem_tlv_read(loop->play->ctl, + mix->src.id, + tlv, sizeof(tlv)); + if (err < 0) { + logit(LOG_CRIT, "Unable to read TLV for '%s': %s\n", id_str(mix->src.id), snd_strerror(err)); + tlv[0] = tlv[1] = 0; + } + err = snd_ctl_elem_tlv_write(loop->capt->ctl, + mix->dst.id, + tlv); + if (err < 0) { + logit(LOG_CRIT, "Unable to write TLV for '%s': %s\n", id_str(mix->src.id), snd_strerror(err)); + return err; + } + } + err = snd_ctl_elem_write(loop->capt->ctl, mix->dst.value); + if (err < 0) { + logit(LOG_CRIT, "Unable to write control value '%s': %s\n", id_str(mix->dst.id), snd_strerror(err)); + return err; + } + return 0; +} + +int control_init(struct loopback *loop) +{ + struct loopback_mixer *mix; + struct loopback_ossmixer *ossmix; + int err; + + for (ossmix = loop->oss_controls; ossmix; ossmix = ossmix->next) + oss_set(loop, ossmix, 0); + for (mix = loop->controls; mix; mix = mix->next) { + err = control_init1(loop->play, &mix->src); + if (err < 0) { + logit(LOG_WARNING, "%s: Disabling playback control '%s'\n", loop->id, id_str(mix->src.id)); + mix->skip = 1; + continue; + } + err = control_init2(loop, mix); + if (err < 0) + return err; + } + for (ossmix = loop->oss_controls; ossmix; ossmix = ossmix->next) { + err = oss_set(loop, ossmix, 1); + if (err < 0) { + ossmix->skip = 1; + logit(LOG_WARNING, "%s: Disabling OSS mixer ID '%s'\n", loop->id, ossmix->oss_id); + } + } + return 0; +} + +int control_done(struct loopback *loop) +{ + struct loopback_mixer *mix; + struct loopback_ossmixer *ossmix; + int err; + + if (loop->capt->ctl == NULL) + return 0; + for (ossmix = loop->oss_controls; ossmix; ossmix = ossmix->next) { + err = oss_set(loop, ossmix, 0); + if (err < 0) + logit(LOG_WARNING, "%s: Unable to remove OSS control '%s'\n", loop->id, ossmix->oss_id); + } + for (mix = loop->controls; mix; mix = mix->next) { + if (mix->skip) + continue; + err = snd_ctl_elem_remove(loop->capt->ctl, mix->dst.id); + if (err < 0) + logit(LOG_WARNING, "%s: Unable to remove control '%s': %s\n", loop->id, id_str(mix->dst.id), snd_strerror(err)); + } + return 0; +} + +static int control_event1(struct loopback *loop, + struct loopback_mixer *mix, + snd_ctl_event_t *ev, + int capture) +{ + unsigned int mask = snd_ctl_event_elem_get_mask(ev); + int err; + + if (mask == SND_CTL_EVENT_MASK_REMOVE) + return 0; + if ((mask & SND_CTL_EVENT_MASK_VALUE) == 0) + return 0; + if (!capture) { + snd_ctl_elem_value_set_id(mix->src.value, mix->src.id); + err = snd_ctl_elem_read(loop->play->ctl, mix->src.value); + if (err < 0) { + logit(LOG_CRIT, "Unable to read control value (event1) '%s': %s\n", id_str(mix->src.id), snd_strerror(err)); + return err; + } + copy_value(&mix->dst, &mix->src); + err = snd_ctl_elem_write(loop->capt->ctl, mix->dst.value); + if (err < 0) { + logit(LOG_CRIT, "Unable to write control value (event1) '%s': %s\n", id_str(mix->dst.id), snd_strerror(err)); + return err; + } + } else { + err = snd_ctl_elem_read(loop->capt->ctl, mix->dst.value); + if (err < 0) { + logit(LOG_CRIT, "Unable to read control value (event2) '%s': %s\n", id_str(mix->dst.id), snd_strerror(err)); + return err; + } + copy_value(&mix->src, &mix->dst); + err = snd_ctl_elem_write(loop->play->ctl, mix->src.value); + if (err < 0) { + logit(LOG_CRIT, "Unable to write control value (event2) '%s': %s\n", id_str(mix->src.id), snd_strerror(err)); + return err; + } + } + return 0; +} + +int control_event(struct loopback_handle *lhandle, snd_ctl_event_t *ev) +{ + snd_ctl_elem_id_t *id2; + struct loopback_mixer *mix; + int capt = lhandle == lhandle->loopback->capt; + int err; + + snd_ctl_elem_id_alloca(&id2); + snd_ctl_event_elem_get_id(ev, id2); + for (mix = lhandle->loopback->controls; mix; mix = mix->next) { + if (mix->skip) + continue; + if (control_id_match(id2, capt ? mix->dst.id : mix->src.id)) { + err = control_event1(lhandle->loopback, mix, ev, capt); + if (err < 0) + return err; + } + } + return 0; +} diff --git a/alsaloop/effect-sweep.c b/alsaloop/effect-sweep.c new file mode 100644 index 0000000..47bdcd5 --- /dev/null +++ b/alsaloop/effect-sweep.c @@ -0,0 +1,129 @@ +/* + * Bandpass filter sweep effect + * Copyright (c) Maarten de Boer + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include + +struct effect_private { + /* filter the sweep variables */ + float lfo,dlfo,fs,fc,BW,C,D,a0,a1,a2,b1,b2,*x[3],*y[3]; + float lfo_depth, lfo_center; + unsigned int channels; +}; + +static int effect_init(struct lookback *loopback, + void *private_data, + snd_pcm_access_t access, + unsigned int channels, + unsigned int rate, + snd_pcm_format_t format) +{ + struct effect_private *priv = private_data; + int i; + +#if __BYTE_ORDER == __LITTLE_ENDIAN + if (format != SND_PCM_FORMAT_S16_LE) + return -EIO; +#elif __BYTE_ORDER == __BIG_ENDIAN + if (format != SND_PCM_FORMAT_S16_BE) + return -EIO; +#else + return -EIO; +#endif + priv->fs = (float) rate; + priv->channels = channels; + for (i = 0; i < 3; i++) { + priv->x[i] = calloc(channels * sizeof(float)); + priv->y[i] = calloc(channels * sizeof(float)); + } + return 0; +} + +static int effect_done(struct loopback *loopback, + void *private_data) +{ + struct effect_private *priv = private_data; + int i; + + for (i = 0; i < 3; i++) { + free(priv->x[i]); + free(priv->y[i]); + } + return 0; +} + +static int effect_apply(struct loopback *loopback, + void *private_data, + const snd_pcm_channel_area_t *areas, + snd_uframes_t offset, + snd_uframes_t frames) +{ + struct effect_private *priv = private_data; + short *samples = (short*)areas[0].addr + offset*priv->channels; + snd_uframes_t i; + + for (i=0; i < frames; i++) { + int chn; + + fc = sin(priv->lfo)*priv->lfo_depth+priv->lfo_center; + priv->lfo += priv->dlfo; + if (priv->lfo>2.*M_PI) priv->lfo -= 2.*M_PI; + priv->C = 1./tan(M_PI*priv->BW/priv->fs); + priv->D = 2.*cos(2*M_PI*fc/fs); + priv->a0 = 1./(1.+priv->C); + priv->a1 = 0; + priv->a2 = -priv->a0; + priv->b1 = -priv->C*priv->D*a0; + priv->b2 = (priv->C-1)*priv->a0; + + for (chn=0; chn < priv->channels; chn++) + { + priv->x[chn][2] = priv->x[chn][1]; + priv->x[chn][1] = priv->x[chn][0]; + + priv->y[chn][2] = priv->y[chn][1]; + priv->y[chn][1] = priv->y[chn][0]; + + priv->x[chn][0] = samples[i*channels+chn]; + priv->y[chn][0] = priv->a0*priv->x[0][chn] + + priv->a1*priv->x[1][chn] + priv->a2*x[2][chn] + - priv->b1*priv->y[1][chn] - priv->b2*y[2][chn]; + samples[i*channels+chn] = priv->y[chn][0]; + } + } + return 0; +} + +void effect_init_sweep(void) +{ + struct effect_private *priv; + + priv = register_effect(effect_init, + effect_apply, + effect_done, + sizeof(struct effectprivate)); + if (priv) { + priv->lfo_center = 2000.; + priv->lfo_depth = 1800.; + priv->lfo_freq = 0.2; + priv->BW = 50; + } +} diff --git a/alsaloop/pcmjob.c b/alsaloop/pcmjob.c new file mode 100644 index 0000000..ffb439b --- /dev/null +++ b/alsaloop/pcmjob.c @@ -0,0 +1,2095 @@ +/* + * A simple PCM loopback utility + * Copyright (c) 2010 by Jaroslav Kysela + * + * Author: Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "alsaloop.h" +#include "os_compat.h" + +#define XRUN_PROFILE_UNKNOWN (-10000000) + +static int set_rate_shift(struct loopback_handle *lhandle, double pitch); +static int get_rate(struct loopback_handle *lhandle); + +#define SYNCTYPE(v) [SYNC_TYPE_##v] = #v + +static const char *sync_types[] = { + SYNCTYPE(NONE), + SYNCTYPE(SIMPLE), + SYNCTYPE(CAPTRATESHIFT), + SYNCTYPE(PLAYRATESHIFT), + SYNCTYPE(SAMPLERATE), + SYNCTYPE(AUTO) +}; + +#define SRCTYPE(v) [SRC_##v] = "SRC_" #v + +#ifdef USE_SAMPLERATE +static const char *src_types[] = { + SRCTYPE(SINC_BEST_QUALITY), + SRCTYPE(SINC_MEDIUM_QUALITY), + SRCTYPE(SINC_FASTEST), + SRCTYPE(ZERO_ORDER_HOLD), + SRCTYPE(LINEAR) +}; +#endif + +static pthread_once_t pcm_open_mutex_once = PTHREAD_ONCE_INIT; +static pthread_mutex_t pcm_open_mutex; + +static void pcm_open_init_mutex(void) +{ + pthread_mutexattr_t attr; + + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&pcm_open_mutex, &attr); + pthread_mutexattr_destroy(&attr); +} + +static inline void pcm_open_lock(void) +{ + pthread_once(&pcm_open_mutex_once, pcm_open_init_mutex); + if (workarounds & WORKAROUND_SERIALOPEN) + pthread_mutex_lock(&pcm_open_mutex); +} + +static inline void pcm_open_unlock(void) +{ + if (workarounds & WORKAROUND_SERIALOPEN) + pthread_mutex_unlock(&pcm_open_mutex); +} + +static inline snd_pcm_uframes_t get_whole_latency(struct loopback *loop) +{ + return loop->latency; +} + +static inline unsigned long long + frames_to_time(unsigned int rate, + snd_pcm_uframes_t frames) +{ + return (frames * 1000000ULL) / rate; +} + +static inline snd_pcm_uframes_t time_to_frames(unsigned int rate, + unsigned long long time) +{ + return (time * rate) / 1000000ULL; +} + +static int setparams_stream(struct loopback_handle *lhandle, + snd_pcm_hw_params_t *params) +{ + snd_pcm_t *handle = lhandle->handle; + int err; + unsigned int rrate; + + err = snd_pcm_hw_params_any(handle, params); + if (err < 0) { + logit(LOG_CRIT, "Broken configuration for %s PCM: no configurations available: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_pcm_hw_params_set_rate_resample(handle, params, lhandle->resample); + if (err < 0) { + logit(LOG_CRIT, "Resample setup failed for %s (val %u): %s\n", lhandle->id, lhandle->resample, snd_strerror(err)); + return err; + } + err = snd_pcm_hw_params_set_access(handle, params, lhandle->access); + if (err < 0) { + logit(LOG_CRIT, "Access type not available for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_pcm_hw_params_set_format(handle, params, lhandle->format); + if (err < 0) { + logit(LOG_CRIT, "Sample format not available for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_pcm_hw_params_set_channels(handle, params, lhandle->channels); + if (err < 0) { + logit(LOG_CRIT, "Channels count (%u) not available for %s: %s\n", lhandle->channels, lhandle->id, snd_strerror(err)); + return err; + } + rrate = lhandle->rate_req; + err = snd_pcm_hw_params_set_rate_near(handle, params, &rrate, 0); + if (err < 0) { + logit(LOG_CRIT, "Rate %uHz not available for %s: %s\n", lhandle->rate_req, lhandle->id, snd_strerror(err)); + return err; + } + rrate = 0; + snd_pcm_hw_params_get_rate(params, &rrate, 0); + lhandle->rate = rrate; + if ( +#ifdef USE_SAMPLERATE + !lhandle->loopback->src_enable && +#endif + rrate != lhandle->rate) { + logit(LOG_CRIT, "Rate does not match (requested %uHz, got %uHz, resample %u)\n", lhandle->rate, rrate, lhandle->resample); + return -EINVAL; + } + lhandle->pitch = (double)lhandle->rate_req / (double)lhandle->rate; + return 0; +} + +static int setparams_bufsize(struct loopback_handle *lhandle, + snd_pcm_hw_params_t *params, + snd_pcm_hw_params_t *tparams, + snd_pcm_uframes_t bufsize) +{ + snd_pcm_t *handle = lhandle->handle; + int err; + snd_pcm_uframes_t periodsize; + snd_pcm_uframes_t buffersize; + snd_pcm_uframes_t last_bufsize = 0; + + if (lhandle->buffer_size_req > 0) { + bufsize = lhandle->buffer_size_req; + last_bufsize = bufsize; + goto __set_it; + } + __again: + if (lhandle->buffer_size_req > 0) { + logit(LOG_CRIT, "Unable to set buffer size %li for %s\n", (long)lhandle->buffer_size, lhandle->id); + return -EIO; + } + if (last_bufsize == bufsize) + bufsize += 4; + last_bufsize = bufsize; + if (bufsize > 10*1024*1024) { + logit(LOG_CRIT, "Buffer size too big\n"); + return -EIO; + } + __set_it: + snd_pcm_hw_params_copy(params, tparams); + periodsize = bufsize * 8; + err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &periodsize); + if (err < 0) { + logit(LOG_CRIT, "Unable to set buffer size %li for %s: %s\n", periodsize, lhandle->id, snd_strerror(err)); + goto __again; + } + snd_pcm_hw_params_get_buffer_size(params, &periodsize); + if (verbose > 6) + snd_output_printf(lhandle->loopback->output, "%s: buffer_size=%li\n", lhandle->id, periodsize); + if (lhandle->period_size_req > 0) + periodsize = lhandle->period_size_req; + else + periodsize /= 8; + err = snd_pcm_hw_params_set_period_size_near(handle, params, &periodsize, 0); + if (err < 0) { + logit(LOG_CRIT, "Unable to set period size %li for %s: %s\n", periodsize, lhandle->id, snd_strerror(err)); + goto __again; + } + snd_pcm_hw_params_get_period_size(params, &periodsize, NULL); + if (verbose > 6) + snd_output_printf(lhandle->loopback->output, "%s: period_size=%li\n", lhandle->id, periodsize); + if (periodsize != bufsize) + bufsize = periodsize; + snd_pcm_hw_params_get_buffer_size(params, &buffersize); + if (periodsize * 2 > buffersize) + goto __again; + lhandle->period_size = periodsize; + lhandle->buffer_size = buffersize; + return 0; +} + +static int setparams_set(struct loopback_handle *lhandle, + snd_pcm_hw_params_t *params, + snd_pcm_sw_params_t *swparams, + snd_pcm_uframes_t bufsize) +{ + snd_pcm_t *handle = lhandle->handle; + int err; + snd_pcm_uframes_t val, period_size, buffer_size; + + err = snd_pcm_hw_params(handle, params); + if (err < 0) { + logit(LOG_CRIT, "Unable to set hw params for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_pcm_sw_params_current(handle, swparams); + if (err < 0) { + logit(LOG_CRIT, "Unable to determine current swparams for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_pcm_sw_params_set_start_threshold(handle, swparams, 0x7fffffff); + if (err < 0) { + logit(LOG_CRIT, "Unable to set start threshold mode for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + snd_pcm_hw_params_get_period_size(params, &period_size, NULL); + snd_pcm_hw_params_get_buffer_size(params, &buffer_size); + if (lhandle->nblock) { + if (lhandle == lhandle->loopback->play) { + val = buffer_size - (2 * period_size - 4); + } else { + val = 4; + } + if (verbose > 6) + snd_output_printf(lhandle->loopback->output, "%s: avail_min1=%li\n", lhandle->id, val); + } else { + if (lhandle == lhandle->loopback->play) { + val = bufsize + bufsize / 2; + if (val > (buffer_size * 3) / 4) + val = (buffer_size * 3) / 4; + val = buffer_size - val; + } else { + val = bufsize / 2; + if (val > buffer_size / 4) + val = buffer_size / 4; + } + if (verbose > 6) + snd_output_printf(lhandle->loopback->output, "%s: avail_min2=%li\n", lhandle->id, val); + } + err = snd_pcm_sw_params_set_avail_min(handle, swparams, val); + if (err < 0) { + logit(LOG_CRIT, "Unable to set avail min for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + snd_pcm_sw_params_get_avail_min(swparams, &lhandle->avail_min); + err = snd_pcm_sw_params(handle, swparams); + if (err < 0) { + logit(LOG_CRIT, "Unable to set sw params for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return 0; +} + +static int increase_playback_avail_min(struct loopback_handle *lhandle) +{ + snd_pcm_t *handle = lhandle->handle; + snd_pcm_sw_params_t *swparams; + struct timespec ts; + int err; + + if (lhandle->avail_min + (lhandle->period_size / 2) > lhandle->buffer_size) { + /* avoid 100% CPU usage for broken plugins */ + ts.tv_sec = 0; + ts.tv_nsec = 10000; + nanosleep(&ts, NULL); + return 0; + } + snd_pcm_sw_params_alloca(&swparams); + err = snd_pcm_sw_params_current(handle, swparams); + if (err < 0) { + logit(LOG_CRIT, "Unable to determine current swparams for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_pcm_sw_params_set_avail_min(handle, swparams, lhandle->avail_min + 4); + if (err < 0) { + logit(LOG_CRIT, "Unable to set avail min for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + snd_pcm_sw_params_get_avail_min(swparams, &lhandle->avail_min); + if (verbose > 6) + snd_output_printf(lhandle->loopback->output, "%s: change avail_min=%li\n", lhandle->id, lhandle->avail_min); + err = snd_pcm_sw_params(handle, swparams); + if (err < 0) { + logit(LOG_CRIT, "Unable to set sw params for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return 0; +} + +static int setparams(struct loopback *loop, snd_pcm_uframes_t bufsize) +{ + int err; + snd_pcm_hw_params_t *pt_params, *ct_params; /* templates with rate, format and channels */ + snd_pcm_hw_params_t *p_params, *c_params; + snd_pcm_sw_params_t *p_swparams, *c_swparams; + + snd_pcm_hw_params_alloca(&p_params); + snd_pcm_hw_params_alloca(&c_params); + snd_pcm_hw_params_alloca(&pt_params); + snd_pcm_hw_params_alloca(&ct_params); + snd_pcm_sw_params_alloca(&p_swparams); + snd_pcm_sw_params_alloca(&c_swparams); + if ((err = setparams_stream(loop->play, pt_params)) < 0) { + logit(LOG_CRIT, "Unable to set parameters for %s stream: %s\n", loop->play->id, snd_strerror(err)); + return err; + } + if ((err = setparams_stream(loop->capt, ct_params)) < 0) { + logit(LOG_CRIT, "Unable to set parameters for %s stream: %s\n", loop->capt->id, snd_strerror(err)); + return err; + } + + if ((err = setparams_bufsize(loop->play, p_params, pt_params, bufsize / loop->play->pitch)) < 0) { + logit(LOG_CRIT, "Unable to set buffer parameters for %s stream: %s\n", loop->play->id, snd_strerror(err)); + return err; + } + if ((err = setparams_bufsize(loop->capt, c_params, ct_params, bufsize / loop->capt->pitch)) < 0) { + logit(LOG_CRIT, "Unable to set buffer parameters for %s stream: %s\n", loop->capt->id, snd_strerror(err)); + return err; + } + + if ((err = setparams_set(loop->play, p_params, p_swparams, bufsize / loop->play->pitch)) < 0) { + logit(LOG_CRIT, "Unable to set sw parameters for %s stream: %s\n", loop->play->id, snd_strerror(err)); + return err; + } + if ((err = setparams_set(loop->capt, c_params, c_swparams, bufsize / loop->capt->pitch)) < 0) { + logit(LOG_CRIT, "Unable to set sw parameters for %s stream: %s\n", loop->capt->id, snd_strerror(err)); + return err; + } + +#if 0 + if (!loop->linked) + if (snd_pcm_link(loop->capt->handle, loop->play->handle) >= 0) + loop->linked = 1; +#endif + if ((err = snd_pcm_prepare(loop->play->handle)) < 0) { + logit(LOG_CRIT, "Prepare %s error: %s\n", loop->play->id, snd_strerror(err)); + return err; + } + if (!loop->linked && (err = snd_pcm_prepare(loop->capt->handle)) < 0) { + logit(LOG_CRIT, "Prepare %s error: %s\n", loop->capt->id, snd_strerror(err)); + return err; + } + + if (verbose) { + snd_pcm_dump(loop->play->handle, loop->output); + snd_pcm_dump(loop->capt->handle, loop->output); + } + return 0; +} + +static void showlatency(snd_output_t *out, size_t latency, unsigned int rate, + char *prefix) +{ + double d; + d = (double)latency / (double)rate; + snd_output_printf(out, "%s %li frames, %.3fus, %.6fms (%.4fHz)\n", prefix, (long)latency, d * 1000000, d * 1000, (double)1 / d); +} + +static long timediff(snd_timestamp_t t1, snd_timestamp_t t2) +{ + signed long l; + + t1.tv_sec -= t2.tv_sec; + if (t1.tv_usec < t2.tv_usec) { + l = ((t1.tv_usec + 1000000) - t2.tv_usec) % 1000000; + t1.tv_sec--; + } else { + l = t1.tv_usec - t2.tv_usec; + } + return (t1.tv_sec * 1000000) + l; +} + +static int getcurtimestamp(snd_timestamp_t *ts) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + ts->tv_sec = tv.tv_sec; + ts->tv_usec = tv.tv_usec; + return 0; +} + +static void xrun_profile0(struct loopback *loop) +{ + snd_pcm_sframes_t pdelay, cdelay; + + if (snd_pcm_delay(loop->play->handle, &pdelay) >= 0 && + snd_pcm_delay(loop->capt->handle, &cdelay) >= 0) { + getcurtimestamp(&loop->xrun_last_update); + loop->xrun_last_pdelay = pdelay; + loop->xrun_last_cdelay = cdelay; + loop->xrun_buf_pcount = loop->play->buf_count; + loop->xrun_buf_ccount = loop->capt->buf_count; +#ifdef USE_SAMPLERATE + loop->xrun_out_frames = loop->src_out_frames; +#endif + } +} + +static inline void xrun_profile(struct loopback *loop) +{ + if (loop->xrun) + xrun_profile0(loop); +} + +static void xrun_stats0(struct loopback *loop) +{ + snd_timestamp_t t; + double expected, last, wake, check, queued = -1, proc, missing = -1; + double maxbuf, pfilled, cfilled, cqueued = -1, avail_min; + double sincejob; + + expected = ((double)loop->latency / + (double)loop->play->rate_req) * 1000; + getcurtimestamp(&t); + last = (double)timediff(t, loop->xrun_last_update) / 1000; + wake = (double)timediff(t, loop->xrun_last_wake) / 1000; + check = (double)timediff(t, loop->xrun_last_check) / 1000; + sincejob = (double)timediff(t, loop->tstamp_start) / 1000; + if (loop->xrun_last_pdelay != XRUN_PROFILE_UNKNOWN) + queued = ((double)loop->xrun_last_pdelay / + (double)loop->play->rate) * 1000; + if (loop->xrun_last_cdelay != XRUN_PROFILE_UNKNOWN) + cqueued = ((double)loop->xrun_last_cdelay / + (double)loop->capt->rate) * 1000; + maxbuf = ((double)loop->play->buffer_size / + (double)loop->play->rate) * 1000; + proc = (double)loop->xrun_max_proctime / 1000; + pfilled = ((double)(loop->xrun_buf_pcount + loop->xrun_out_frames) / + (double)loop->play->rate) * 1000; + cfilled = ((double)loop->xrun_buf_ccount / + (double)loop->capt->rate) * 1000; + avail_min = (((double)loop->play->buffer_size - + (double)loop->play->avail_min ) / + (double)loop->play->rate) * 1000; + avail_min = expected - avail_min; + if (queued >= 0) + missing = last - queued; + if (missing >= 0 && loop->xrun_max_missing < missing) + loop->xrun_max_missing = missing; + loop->xrun_max_proctime = 0; + getcurtimestamp(&t); + logit(LOG_INFO, " last write before %.4fms, queued %.4fms/%.4fms -> missing %.4fms\n", last, queued, cqueued, missing); + logit(LOG_INFO, " expected %.4fms, processing %.4fms, max missing %.4fms\n", expected, proc, loop->xrun_max_missing); + logit(LOG_INFO, " last wake %.4fms, last check %.4fms, avail_min %.4fms\n", wake, check, avail_min); + logit(LOG_INFO, " max buf %.4fms, pfilled %.4fms, cfilled %.4fms\n", maxbuf, pfilled, cfilled); + logit(LOG_INFO, " job started before %.4fms\n", sincejob); +} + +static inline void xrun_stats(struct loopback *loop) +{ + if (loop->xrun) + xrun_stats0(loop); +} + +static inline snd_pcm_uframes_t buf_avail(struct loopback_handle *lhandle) +{ + return lhandle->buf_size - lhandle->buf_count; +} + +static void buf_remove(struct loopback *loop, snd_pcm_uframes_t count) +{ + /* remove samples from the capture buffer */ + if (count <= 0) + return; + if (loop->play->buf == loop->capt->buf) { + if (count < loop->capt->buf_count) + loop->capt->buf_count -= count; + else + loop->capt->buf_count = 0; + } +} + +#if 0 +static void buf_add_copy(struct loopback *loop) +{ + struct loopback_handle *capt = loop->capt; + struct loopback_handle *play = loop->play; + snd_pcm_uframes_t count, count1, cpos, ppos; + + count = capt->buf_count; + cpos = capt->buf_pos - count; + if (cpos > capt->buf_size) + cpos += capt->buf_size; + ppos = (play->buf_pos + play->buf_count) % play->buf_size; + while (count > 0) { + count1 = count; + if (count1 + cpos > capt->buf_size) + count1 = capt->buf_size - cpos; + if (count1 > buf_avail(play)) + count1 = buf_avail(play); + if (count1 + ppos > play->buf_size) + count1 = play->buf_size - ppos; + if (count1 == 0) + break; + memcpy(play->buf + ppos * play->frame_size, + capt->buf + cpos * capt->frame_size, + count1 * capt->frame_size); + play->buf_count += count1; + capt->buf_count -= count1; + ppos += count1; + ppos %= play->buf_size; + cpos += count1; + cpos %= capt->buf_size; + count -= count1; + } +} +#endif + +#ifdef USE_SAMPLERATE +static void buf_add_src(struct loopback *loop) +{ + struct loopback_handle *capt = loop->capt; + struct loopback_handle *play = loop->play; + float *old_data_out; + snd_pcm_uframes_t count, pos, count1, pos1; + count = capt->buf_count; + pos = 0; + pos1 = capt->buf_pos - count; + if (pos1 > capt->buf_size) + pos1 += capt->buf_size; + while (count > 0) { + count1 = count; + if (count1 + pos1 > capt->buf_size) + count1 = capt->buf_size - pos1; + if (capt->format == SND_PCM_FORMAT_S32) + src_int_to_float_array((int *)(capt->buf + + pos1 * capt->frame_size), + (float *)loop->src_data.data_in + + pos * capt->channels, + count1 * capt->channels); + else + src_short_to_float_array((short *)(capt->buf + + pos1 * capt->frame_size), + (float *)loop->src_data.data_in + + pos * capt->channels, + count1 * capt->channels); + count -= count1; + pos += count1; + pos1 += count1; + pos1 %= capt->buf_size; + } + loop->src_data.input_frames = pos; + loop->src_data.output_frames = play->buf_size - + loop->src_out_frames; + loop->src_data.end_of_input = 0; + old_data_out = loop->src_data.data_out; + loop->src_data.data_out = old_data_out + loop->src_out_frames; + src_process(loop->src_state, &loop->src_data); + loop->src_data.data_out = old_data_out; + capt->buf_count -= loop->src_data.input_frames_used; + count = loop->src_data.output_frames_gen + + loop->src_out_frames; + pos = 0; + pos1 = (play->buf_pos + play->buf_count) % play->buf_size; + while (count > 0) { + count1 = count; + if (count1 + pos1 > play->buf_size) + count1 = play->buf_size - pos1; + if (count1 > buf_avail(play)) + count1 = buf_avail(play); + if (count1 == 0) + break; + if (capt->format == SND_PCM_FORMAT_S32) + src_float_to_int_array(loop->src_data.data_out + + pos * play->channels, + (int *)(play->buf + + pos1 * play->frame_size), + count1 * play->channels); + else + src_float_to_short_array(loop->src_data.data_out + + pos * play->channels, + (short *)(play->buf + + pos1 * play->frame_size), + count1 * play->channels); + play->buf_count += count1; + count -= count1; + pos += count1; + pos1 += count1; + pos1 %= play->buf_size; + } +#if 0 + printf("src: pos = %li, gen = %li, out = %li, count = %li\n", + (long)pos, (long)loop->src_data.output_frames_gen, + (long)loop->src_out_frames, play->buf_count); +#endif + loop->src_out_frames = (loop->src_data.output_frames_gen + + loop->src_out_frames) - pos; + if (loop->src_out_frames > 0) { + memmove(loop->src_data.data_out, + loop->src_data.data_out + pos * play->channels, + loop->src_out_frames * play->channels * sizeof(float)); + } +} +#else +static void buf_add_src(struct loopback *loop ATTRIBUTE_UNUSED) +{ +} +#endif + +static void buf_add(struct loopback *loop, snd_pcm_uframes_t count) +{ + /* copy samples from capture to playback buffer */ + if (count <= 0) + return; + if (loop->play->buf == loop->capt->buf) { + loop->play->buf_count += count; + } else { + buf_add_src(loop); + } +} + +static int xrun(struct loopback_handle *lhandle) +{ + int err; + + if (lhandle == lhandle->loopback->play) { + logit(LOG_DEBUG, "underrun for %s\n", lhandle->id); + xrun_stats(lhandle->loopback); + if ((err = snd_pcm_prepare(lhandle->handle)) < 0) + return err; + lhandle->xrun_pending = 1; + } else { + logit(LOG_DEBUG, "overrun for %s\n", lhandle->id); + xrun_stats(lhandle->loopback); + if ((err = snd_pcm_prepare(lhandle->handle)) < 0) + return err; + lhandle->xrun_pending = 1; + } + return 0; +} + +static int suspend(struct loopback_handle *lhandle) +{ + int err; + + while ((err = snd_pcm_resume(lhandle->handle)) == -EAGAIN) + usleep(1); + if (err < 0) + return xrun(lhandle); + return 0; +} + +static int readit(struct loopback_handle *lhandle) +{ + snd_pcm_sframes_t r, res = 0; + snd_pcm_sframes_t avail; + int err; + + avail = snd_pcm_avail_update(lhandle->handle); + if (avail == -EPIPE) { + return xrun(lhandle); + } else if (avail == -ESTRPIPE) { + if ((err = suspend(lhandle)) < 0) + return err; + } + if ((snd_pcm_uframes_t)avail > buf_avail(lhandle)) { + lhandle->buf_over += avail - buf_avail(lhandle); + avail = buf_avail(lhandle); + } else if (avail == 0) { + if (snd_pcm_state(lhandle->handle) == SND_PCM_STATE_DRAINING) { + lhandle->loopback->reinit = 1; + return 0; + } + } + while (avail > 0) { + r = buf_avail(lhandle); + if (r + lhandle->buf_pos > lhandle->buf_size) + r = lhandle->buf_size - lhandle->buf_pos; + if (r > avail) + r = avail; + r = snd_pcm_readi(lhandle->handle, + lhandle->buf + + lhandle->buf_pos * + lhandle->frame_size, r); + if (r == 0) + return res; + if (r < 0) { + if (r == -EPIPE) { + err = xrun(lhandle); + return res > 0 ? res : err; + } else if (r == -ESTRPIPE) { + if ((err = suspend(lhandle)) < 0) + return res > 0 ? res : err; + r = 0; + } else { + return res > 0 ? res : r; + } + } +#ifdef FILE_CWRITE + if (lhandle->loopback->cfile) + fwrite(lhandle->buf + lhandle->buf_pos * lhandle->frame_size, + r, lhandle->frame_size, lhandle->loopback->cfile); +#endif + res += r; + if (lhandle->max < (snd_pcm_uframes_t)res) + lhandle->max = res; + lhandle->counter += r; + lhandle->buf_count += r; + lhandle->buf_pos += r; + lhandle->buf_pos %= lhandle->buf_size; + avail -= r; + } + return res; +} + +static int writeit(struct loopback_handle *lhandle) +{ + snd_pcm_sframes_t avail; + snd_pcm_sframes_t r, res = 0; + int err; + + __again: + avail = snd_pcm_avail_update(lhandle->handle); + if (avail == -EPIPE) { + if ((err = xrun(lhandle)) < 0) + return err; + return res; + } else if (avail == -ESTRPIPE) { + if ((err = suspend(lhandle)) < 0) + return err; + goto __again; + } + while (avail > 0 && lhandle->buf_count > 0) { + r = lhandle->buf_count; + if (r + lhandle->buf_pos > lhandle->buf_size) + r = lhandle->buf_size - lhandle->buf_pos; + if (r > avail) + r = avail; + r = snd_pcm_writei(lhandle->handle, + lhandle->buf + + lhandle->buf_pos * + lhandle->frame_size, r); + if (r <= 0) { + if (r == -EPIPE) { + if ((err = xrun(lhandle)) < 0) + return err; + return res; + } else if (r == -ESTRPIPE) { + } + return res > 0 ? res : r; + } +#ifdef FILE_PWRITE + if (lhandle->loopback->pfile) + fwrite(lhandle->buf + lhandle->buf_pos * lhandle->frame_size, + r, lhandle->frame_size, lhandle->loopback->pfile); +#endif + res += r; + lhandle->counter += r; + lhandle->buf_count -= r; + lhandle->buf_pos += r; + lhandle->buf_pos %= lhandle->buf_size; + xrun_profile(lhandle->loopback); + if (lhandle->loopback->stop_pending) { + lhandle->loopback->stop_count += r; + if (lhandle->loopback->stop_count * lhandle->pitch > + lhandle->loopback->latency * 3) { + lhandle->loopback->stop_pending = 0; + lhandle->loopback->reinit = 1; + break; + } + } + } + return res; +} + +static snd_pcm_sframes_t remove_samples(struct loopback *loop, + int capture_preferred, + snd_pcm_sframes_t count) +{ + struct loopback_handle *play = loop->play; + struct loopback_handle *capt = loop->capt; + + if (loop->play->buf == loop->capt->buf) { + if ((snd_pcm_uframes_t)count > loop->play->buf_count) + count = loop->play->buf_count; + if ((snd_pcm_uframes_t)count > loop->capt->buf_count) + count = loop->capt->buf_count; + capt->buf_count -= count; + play->buf_pos += count; + play->buf_pos %= play->buf_size; + play->buf_count -= count; + return count; + } + if (capture_preferred) { + if ((snd_pcm_uframes_t)count > capt->buf_count) + count = capt->buf_count; + capt->buf_count -= count; + } else { + if ((snd_pcm_uframes_t)count > play->buf_count) + count = play->buf_count; + play->buf_count -= count; + } + return count; +} + +static int xrun_sync(struct loopback *loop) +{ + struct loopback_handle *play = loop->play; + struct loopback_handle *capt = loop->capt; + snd_pcm_sframes_t fill = get_whole_latency(loop); + snd_pcm_sframes_t pdelay, cdelay, delay1, pdelay1, cdelay1, diff; + int err; + + __again: + if (verbose > 5) + snd_output_printf(loop->output, "%s: xrun sync %i %i\n", loop->id, capt->xrun_pending, play->xrun_pending); + if (capt->xrun_pending) { + __pagain: + capt->xrun_pending = 0; + if ((err = snd_pcm_prepare(capt->handle)) < 0) { + logit(LOG_CRIT, "%s prepare failed: %s\n", capt->id, snd_strerror(err)); + return err; + } + if ((err = snd_pcm_start(capt->handle)) < 0) { + logit(LOG_CRIT, "%s start failed: %s\n", capt->id, snd_strerror(err)); + return err; + } + } else { + diff = readit(capt); + buf_add(loop, diff); + if (capt->xrun_pending) + goto __pagain; + } + /* skip additional playback samples */ + if ((err = snd_pcm_delay(capt->handle, &cdelay)) < 0) { + if (err == -EPIPE) { + capt->xrun_pending = 1; + goto __again; + } + if (err == -ESTRPIPE) { + err = suspend(capt); + if (err < 0) + return err; + goto __again; + } + logit(LOG_CRIT, "%s capture delay failed: %s\n", capt->id, snd_strerror(err)); + return err; + } + if ((err = snd_pcm_delay(play->handle, &pdelay)) < 0) { + if (err == -EPIPE) { + pdelay = 0; + play->xrun_pending = 1; + } else if (err == -ESTRPIPE) { + err = suspend(play); + if (err < 0) + return err; + goto __again; + } else { + logit(LOG_CRIT, "%s playback delay failed: %s\n", play->id, snd_strerror(err)); + return err; + } + } + capt->counter = cdelay; + play->counter = pdelay; + if (play->buf != capt->buf) + cdelay += capt->buf_count; + pdelay += play->buf_count; +#ifdef USE_SAMPLERATE + pdelay += loop->src_out_frames; +#endif + cdelay1 = cdelay * capt->pitch; + pdelay1 = pdelay * play->pitch; + delay1 = cdelay1 + pdelay1; + capt->total_queued = 0; + play->total_queued = 0; + loop->total_queued_count = 0; + loop->pitch_diff = loop->pitch_diff_min = loop->pitch_diff_max = 0; + if (verbose > 6) { + snd_output_printf(loop->output, + "sync: cdelay=%li(%li), pdelay=%li(%li), fill=%li (delay=%li)" +#ifdef USE_SAMPLERATE + ", src_out=%li" +#endif + "\n", + (long)cdelay, (long)cdelay1, (long)pdelay, (long)pdelay1, + (long)fill, (long)delay1 +#ifdef USE_SAMPLERATE + , (long)loop->src_out_frames +#endif + ); + snd_output_printf(loop->output, + "sync: cbufcount=%li, pbufcount=%li\n", + (long)capt->buf_count, (long)play->buf_count); + } + if (delay1 > fill && capt->counter > 0) { + if ((err = snd_pcm_drop(capt->handle)) < 0) + return err; + if ((err = snd_pcm_prepare(capt->handle)) < 0) + return err; + if ((err = snd_pcm_start(capt->handle)) < 0) + return err; + diff = remove_samples(loop, 1, (delay1 - fill) / capt->pitch); + if (verbose > 6) + snd_output_printf(loop->output, + "sync: capt stop removed %li samples\n", (long)diff); + goto __again; + } + if (delay1 > fill) { + diff = (delay1 - fill) / play->pitch; + if ((snd_pcm_uframes_t)diff > play->buf_count) + diff = play->buf_count; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: removing %li playback samples, delay1=%li\n", (long)diff, (long)delay1); + diff = remove_samples(loop, 0, diff); + pdelay -= diff; + pdelay1 = pdelay * play->pitch; + delay1 = cdelay1 + pdelay1; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: removed %li playback samples, delay1=%li\n", (long)diff, (long)delay1); + } + if (delay1 > fill) { + diff = (delay1 - fill) / capt->pitch; + if ((snd_pcm_uframes_t)diff > capt->buf_count) + diff = capt->buf_count; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: removing %li captured samples, delay1=%li\n", (long)diff, (long)delay1); + diff -= remove_samples(loop, 1, diff); + cdelay -= diff; + cdelay1 = cdelay * capt->pitch; + delay1 = cdelay1 + pdelay1; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: removed %li captured samples, delay1=%li\n", (long)diff, (long)delay1); + } + if (play->xrun_pending) { + play->xrun_pending = 0; + diff = (fill - delay1) / play->pitch; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: xrun_pending, silence filling %li / buf_count=%li\n", (long)diff, play->buf_count); + if (fill > delay1 && play->buf_count < (snd_pcm_uframes_t)diff) { + diff = diff - play->buf_count; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: playback silence added %li samples\n", (long)diff); + play->buf_pos -= diff; + play->buf_pos %= play->buf_size; + err = snd_pcm_format_set_silence(play->format, play->buf + play->buf_pos * play->frame_size, + diff * play->channels); + if (err < 0) + return err; + play->buf_count += diff; + } + if ((err = snd_pcm_prepare(play->handle)) < 0) { + logit(LOG_CRIT, "%s prepare failed: %s\n", play->id, snd_strerror(err)); + return err; + } + delay1 = writeit(play); + if (verbose > 6) + snd_output_printf(loop->output, + "sync: playback wrote %li samples\n", (long)delay1); + if (delay1 > diff) { + buf_remove(loop, delay1 - diff); + if (verbose > 6) + snd_output_printf(loop->output, + "sync: playback buf_remove %li samples\n", (long)(delay1 - diff)); + } + if ((err = snd_pcm_start(play->handle)) < 0) { + logit(LOG_CRIT, "%s start failed: %s\n", play->id, snd_strerror(err)); + return err; + } + } else if (delay1 < (snd_pcm_sframes_t)fill) { + diff = (fill - delay1) / play->pitch; + while (diff > 0) { + delay1 = play->buf_size - play->buf_pos; + if (verbose > 6) + snd_output_printf(loop->output, + "sync: playback short, silence filling %li / buf_count=%li\n", (long)delay1, play->buf_count); + if (delay1 > diff) + delay1 = diff; + err = snd_pcm_format_set_silence(play->format, play->buf + play->buf_pos * play->frame_size, + delay1 * play->channels); + if (err < 0) + return err; + play->buf_pos += delay1; + play->buf_pos %= play->buf_size; + play->buf_count += delay1; + diff -= delay1; + } + writeit(play); + } + if (verbose > 5) { + snd_output_printf(loop->output, "%s: xrun sync ok\n", loop->id); + if (verbose > 6) { + if (snd_pcm_delay(capt->handle, &cdelay) < 0) + cdelay = -1; + if (snd_pcm_delay(play->handle, &pdelay) < 0) + pdelay = -1; + if (play->buf != capt->buf) + cdelay += capt->buf_count; + pdelay += play->buf_count; +#ifdef USE_SAMPLERATE + pdelay += loop->src_out_frames; +#endif + cdelay1 = cdelay * capt->pitch; + pdelay1 = pdelay * play->pitch; + delay1 = cdelay1 + pdelay1; + snd_output_printf(loop->output, "%s: sync verify: %li\n", loop->id, delay1); + } + } + loop->xrun_max_proctime = 0; + return 0; +} + +static int set_notify(struct loopback_handle *lhandle, int enable) +{ + int err; + + if (lhandle->ctl_notify == NULL) + return 0; + snd_ctl_elem_value_set_boolean(lhandle->ctl_notify, 0, enable); + err = snd_ctl_elem_write(lhandle->ctl, lhandle->ctl_notify); + if (err < 0) { + logit(LOG_CRIT, "Cannot set PCM Notify element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + err = snd_ctl_elem_read(lhandle->ctl, lhandle->ctl_notify); + if (err < 0) { + logit(LOG_CRIT, "Cannot get PCM Notify element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return 0; +} + +static int set_rate_shift(struct loopback_handle *lhandle, double pitch) +{ + int err; + + if (lhandle->ctl_rate_shift) { + snd_ctl_elem_value_set_integer(lhandle->ctl_rate_shift, 0, pitch * 100000); + err = snd_ctl_elem_write(lhandle->ctl, lhandle->ctl_rate_shift); + } else if (lhandle->ctl_pitch) { + // 'Playback/Capture Pitch 1000000' requires reciprocal to pitch + snd_ctl_elem_value_set_integer(lhandle->ctl_pitch, 0, (1 / pitch) * 1000000); + err = snd_ctl_elem_write(lhandle->ctl, lhandle->ctl_pitch); + } else { + return 0; + } + if (err < 0) { + logit(LOG_CRIT, "Cannot set PCM Rate Shift element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return 0; +} + +void update_pitch(struct loopback *loop) +{ + double pitch = loop->pitch; + +#ifdef USE_SAMPLERATE + if (loop->sync == SYNC_TYPE_SAMPLERATE) { + loop->src_data.src_ratio = (double)1.0 / (pitch * + loop->play->pitch * loop->capt->pitch); + if (verbose > 2) + snd_output_printf(loop->output, "%s: Samplerate src_ratio update1: %.8f\n", loop->id, loop->src_data.src_ratio); + } else +#endif + if (loop->sync == SYNC_TYPE_CAPTRATESHIFT) { + set_rate_shift(loop->capt, pitch); +#ifdef USE_SAMPLERATE + if (loop->use_samplerate) { + loop->src_data.src_ratio = + (double)1.0 / + (loop->play->pitch * loop->capt->pitch); + if (verbose > 2) + snd_output_printf(loop->output, "%s: Samplerate src_ratio update2: %.8f\n", loop->id, loop->src_data.src_ratio); + } +#endif + } + else if (loop->sync == SYNC_TYPE_PLAYRATESHIFT) { + // pitch is capture-based, playback side requires reciprocal + set_rate_shift(loop->play, 1 / pitch); +#ifdef USE_SAMPLERATE + if (loop->use_samplerate) { + loop->src_data.src_ratio = + (double)1.0 / + (loop->play->pitch * loop->capt->pitch); + if (verbose > 2) + snd_output_printf(loop->output, "%s: Samplerate src_ratio update3: %.8f\n", loop->id, loop->src_data.src_ratio); + } +#endif + } + if (verbose) + snd_output_printf(loop->output, "New pitch for %s: %.8f (min/max samples = %li/%li)\n", loop->id, pitch, loop->pitch_diff_min, loop->pitch_diff_max); +} + +static int get_active(struct loopback_handle *lhandle) +{ + int err; + + if (lhandle->ctl_active == NULL) + return 0; + err = snd_ctl_elem_read(lhandle->ctl, lhandle->ctl_active); + if (err < 0) { + logit(LOG_CRIT, "Cannot get PCM Slave Active element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return snd_ctl_elem_value_get_boolean(lhandle->ctl_active, 0); +} + +static int get_format(struct loopback_handle *lhandle) +{ + int err; + + if (lhandle->ctl_format == NULL) + return 0; + err = snd_ctl_elem_read(lhandle->ctl, lhandle->ctl_format); + if (err < 0) { + logit(LOG_CRIT, "Cannot get PCM Format element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return snd_ctl_elem_value_get_integer(lhandle->ctl_format, 0); +} + +static int get_rate(struct loopback_handle *lhandle) +{ + int err; + + if (lhandle->ctl_rate == NULL) + return 0; + err = snd_ctl_elem_read(lhandle->ctl, lhandle->ctl_rate); + if (err < 0) { + logit(LOG_CRIT, "Cannot get PCM Rate element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return snd_ctl_elem_value_get_integer(lhandle->ctl_rate, 0); +} + +static int get_channels(struct loopback_handle *lhandle) +{ + int err; + + if (lhandle->ctl_channels == NULL) + return 0; + err = snd_ctl_elem_read(lhandle->ctl, lhandle->ctl_channels); + if (err < 0) { + logit(LOG_CRIT, "Cannot get PCM Channels element for %s: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + return snd_ctl_elem_value_get_integer(lhandle->ctl_channels, 0); +} + +static int openctl_elem_id(struct loopback_handle *lhandle, snd_ctl_elem_id_t *id, + snd_ctl_elem_value_t **elem) +{ + int err; + + if (snd_ctl_elem_value_malloc(elem) < 0) { + *elem = NULL; + return -ENOMEM; + } + snd_ctl_elem_value_set_id(*elem, id); + snd_ctl_elem_value_set_interface(*elem, SND_CTL_ELEM_IFACE_PCM); + err = snd_ctl_elem_read(lhandle->ctl, *elem); + if (err < 0) { + snd_ctl_elem_value_free(*elem); + *elem = NULL; + return err; + } else { + snd_output_printf(lhandle->loopback->output, + "Opened PCM element %s of %s, device %d, subdevice %d\n", + snd_ctl_elem_id_get_name(id), snd_ctl_name(lhandle->ctl), + snd_ctl_elem_id_get_device(id), + snd_ctl_elem_id_get_subdevice(id)); + return 0; + } +} + +static int openctl_elem(struct loopback_handle *lhandle, + int device, int subdevice, + const char *name, + snd_ctl_elem_value_t **elem) +{ + snd_ctl_elem_id_t *id; + + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_id_set_device(id, device); + snd_ctl_elem_id_set_subdevice(id, subdevice); + snd_ctl_elem_id_set_name(id, name); + return openctl_elem_id(lhandle, id, elem); +} + +static int openctl_elem_ascii(struct loopback_handle *lhandle, char *ascii_name, + snd_ctl_elem_value_t **elem) +{ + snd_ctl_elem_id_t *id; + + snd_ctl_elem_id_alloca(&id); + if (snd_ctl_ascii_elem_id_parse(id, ascii_name)) { + fprintf(stderr, "Wrong control identifier: %s\n", ascii_name); + return -EINVAL; + } + return openctl_elem_id(lhandle, id, elem); +} + +static int openctl(struct loopback_handle *lhandle, int device, int subdevice) +{ + int err; + + lhandle->ctl_rate_shift = NULL; + if (lhandle->loopback->play == lhandle) { + // play only + if (lhandle->prateshift_name) { + err = openctl_elem_ascii(lhandle, lhandle->prateshift_name, + &lhandle->ctl_rate_shift); + if (err < 0) { + logit(LOG_CRIT, "Unable to open playback PCM Rate Shift elem '%s'.\n", + lhandle->prateshift_name); + exit(EXIT_FAILURE); + } + } else + openctl_elem(lhandle, device, subdevice, "Playback Pitch 1000000", + &lhandle->ctl_pitch); + set_rate_shift(lhandle, 1); + if (lhandle->loopback->controls) + goto __events; + return 0; + } + // capture only + openctl_elem(lhandle, device, subdevice, "PCM Notify", + &lhandle->ctl_notify); + openctl_elem(lhandle, device, subdevice, "PCM Rate Shift 100000", + &lhandle->ctl_rate_shift); + openctl_elem(lhandle, device, subdevice, "Capture Pitch 1000000", + &lhandle->ctl_pitch); + set_rate_shift(lhandle, 1); + openctl_elem(lhandle, device, subdevice, "PCM Slave Active", + &lhandle->ctl_active); + openctl_elem(lhandle, device, subdevice, "PCM Slave Format", + &lhandle->ctl_format); + openctl_elem(lhandle, device, subdevice, "PCM Slave Rate", + &lhandle->ctl_rate); + openctl_elem(lhandle, device, subdevice, "PCM Slave Channels", + &lhandle->ctl_channels); + if ((lhandle->ctl_active && + lhandle->ctl_format && + lhandle->ctl_rate && + lhandle->ctl_channels) || + lhandle->loopback->controls) { + __events: + if ((err = snd_ctl_poll_descriptors_count(lhandle->ctl)) < 0) + lhandle->ctl_pollfd_count = 0; + else + lhandle->ctl_pollfd_count = err; + if (snd_ctl_subscribe_events(lhandle->ctl, 1) < 0) + lhandle->ctl_pollfd_count = 0; + } + return 0; +} + +static int openit(struct loopback_handle *lhandle) +{ + snd_pcm_info_t *info; + int stream = lhandle == lhandle->loopback->play ? + SND_PCM_STREAM_PLAYBACK : + SND_PCM_STREAM_CAPTURE; + int err, card, device, subdevice; + pcm_open_lock(); + err = snd_pcm_open(&lhandle->handle, lhandle->device, stream, SND_PCM_NONBLOCK); + pcm_open_unlock(); + if (err < 0) { + logit(LOG_CRIT, "%s open error: %s\n", lhandle->id, snd_strerror(err)); + return err; + } + if ((err = snd_pcm_info_malloc(&info)) < 0) + return err; + if ((err = snd_pcm_info(lhandle->handle, info)) < 0) { + snd_pcm_info_free(info); + return err; + } + card = snd_pcm_info_get_card(info); + device = snd_pcm_info_get_device(info); + subdevice = snd_pcm_info_get_subdevice(info); + snd_pcm_info_free(info); + lhandle->card_number = card; + lhandle->ctl = NULL; + if (card >= 0 || lhandle->ctldev) { + char name[16], *dev = lhandle->ctldev; + if (dev == NULL) { + sprintf(name, "hw:%i", card); + dev = name; + } + pcm_open_lock(); + err = snd_ctl_open(&lhandle->ctl, dev, SND_CTL_NONBLOCK); + pcm_open_unlock(); + if (err < 0) { + logit(LOG_CRIT, "%s [%s] ctl open error: %s\n", lhandle->id, dev, snd_strerror(err)); + lhandle->ctl = NULL; + } + if (lhandle->ctl) + openctl(lhandle, device, subdevice); + } + return 0; +} + +static int freeit(struct loopback_handle *lhandle) +{ + free(lhandle->buf); + lhandle->buf = NULL; + return 0; +} + +static int closeit(struct loopback_handle *lhandle) +{ + int err = 0; + + set_rate_shift(lhandle, 1); + if (lhandle->ctl_rate_shift) + snd_ctl_elem_value_free(lhandle->ctl_rate_shift); + lhandle->ctl_rate_shift = NULL; + if (lhandle->ctl_pitch) + snd_ctl_elem_value_free(lhandle->ctl_pitch); + lhandle->ctl_pitch = NULL; + if (lhandle->ctl) + err = snd_ctl_close(lhandle->ctl); + lhandle->ctl = NULL; + if (lhandle->handle) + err = snd_pcm_close(lhandle->handle); + lhandle->handle = NULL; + return err; +} + +static int init_handle(struct loopback_handle *lhandle, int alloc) +{ + snd_pcm_uframes_t lat; + lhandle->frame_size = (snd_pcm_format_physical_width(lhandle->format) + / 8) * lhandle->channels; + lhandle->sync_point = lhandle->rate * 15; /* every 15 seconds */ + lat = lhandle->loopback->latency; + if (lhandle->buffer_size > lat) + lat = lhandle->buffer_size; + lhandle->buf_size = lat * 2; + if (alloc) { + lhandle->buf = calloc(1, lhandle->buf_size * lhandle->frame_size); + if (lhandle->buf == NULL) + return -ENOMEM; + } + return 0; +} + +int pcmjob_init(struct loopback *loop) +{ + int err; + char id[128]; + +#ifdef FILE_CWRITE + loop->cfile = fopen(FILE_CWRITE, "w+"); +#endif +#ifdef FILE_PWRITE + loop->pfile = fopen(FILE_PWRITE, "w+"); +#endif + if ((err = openit(loop->play)) < 0) + goto __error; + if ((err = openit(loop->capt)) < 0) + goto __error; + snprintf(id, sizeof(id), "%s/%s", loop->play->id, loop->capt->id); + id[sizeof(id)-1] = '\0'; + loop->id = strdup(id); + if (loop->sync == SYNC_TYPE_AUTO && (loop->capt->ctl_rate_shift || loop->capt->ctl_pitch)) + loop->sync = SYNC_TYPE_CAPTRATESHIFT; + if (loop->sync == SYNC_TYPE_AUTO && (loop->play->ctl_rate_shift || loop->play->ctl_pitch)) + loop->sync = SYNC_TYPE_PLAYRATESHIFT; +#ifdef USE_SAMPLERATE + if (loop->sync == SYNC_TYPE_AUTO && loop->src_enable) + loop->sync = SYNC_TYPE_SAMPLERATE; +#endif + if (loop->sync == SYNC_TYPE_AUTO) + loop->sync = SYNC_TYPE_SIMPLE; + if (loop->slave == SLAVE_TYPE_AUTO && + loop->capt->ctl_notify && + loop->capt->ctl_active && + loop->capt->ctl_format && + loop->capt->ctl_rate && + loop->capt->ctl_channels) + loop->slave = SLAVE_TYPE_ON; + if (loop->slave == SLAVE_TYPE_ON) { + err = set_notify(loop->capt, 1); + if (err < 0) + goto __error; + if (loop->capt->ctl_notify == NULL || + snd_ctl_elem_value_get_boolean(loop->capt->ctl_notify, 0) == 0) { + logit(LOG_CRIT, "unable to enable slave mode for %s\n", loop->id); + err = -EINVAL; + goto __error; + } + } + err = control_init(loop); + if (err < 0) + goto __error; + return 0; + __error: + pcmjob_done(loop); + return err; +} + +static void freeloop(struct loopback *loop) +{ +#ifdef USE_SAMPLERATE + if (loop->use_samplerate) { + if (loop->src_state) + src_delete(loop->src_state); + loop->src_state = NULL; + free((void *)loop->src_data.data_in); + loop->src_data.data_in = NULL; + free(loop->src_data.data_out); + loop->src_data.data_out = NULL; + } +#endif + if (loop->play->buf == loop->capt->buf) + loop->play->buf = NULL; + freeit(loop->play); + freeit(loop->capt); +} + +int pcmjob_done(struct loopback *loop) +{ + control_done(loop); + closeit(loop->play); + closeit(loop->capt); + freeloop(loop); + free(loop->id); + loop->id = NULL; +#ifdef FILE_PWRITE + if (loop->pfile) { + fclose(loop->pfile); + loop->pfile = NULL; + } +#endif +#ifdef FILE_CWRITE + if (loop->cfile) { + fclose(loop->cfile); + loop->cfile = NULL; + } +#endif + return 0; +} + +static void lhandle_start(struct loopback_handle *lhandle) +{ + lhandle->buf_pos = 0; + lhandle->buf_count = 0; + lhandle->counter = 0; + lhandle->total_queued = 0; +} + +static void fix_format(struct loopback *loop, int force) +{ + snd_pcm_format_t format = loop->capt->format; + + if (!force && loop->sync != SYNC_TYPE_SAMPLERATE) + return; + if (format == SND_PCM_FORMAT_S16 || + format == SND_PCM_FORMAT_S32) + return; + if (snd_pcm_format_width(format) > 16) + format = SND_PCM_FORMAT_S32; + else + format = SND_PCM_FORMAT_S16; + loop->capt->format = format; + loop->play->format = format; +} + +int pcmjob_start(struct loopback *loop) +{ + snd_pcm_uframes_t count; + int err; + + loop->pollfd_count = loop->play->ctl_pollfd_count + + loop->capt->ctl_pollfd_count; + if ((err = snd_pcm_poll_descriptors_count(loop->play->handle)) < 0) + goto __error; + loop->play->pollfd_count = err; + loop->pollfd_count += err; + if ((err = snd_pcm_poll_descriptors_count(loop->capt->handle)) < 0) + goto __error; + loop->capt->pollfd_count = err; + loop->pollfd_count += err; + if (loop->slave == SLAVE_TYPE_ON) { + err = get_active(loop->capt); + if (err < 0) + goto __error; + if (err == 0) /* stream is not active */ + return 0; + err = get_format(loop->capt); + if (err < 0) + goto __error; + loop->play->format = loop->capt->format = err; + fix_format(loop, 0); + err = get_rate(loop->capt); + if (err < 0) + goto __error; + loop->play->rate_req = loop->capt->rate_req = err; + err = get_channels(loop->capt); + if (err < 0) + goto __error; + loop->play->channels = loop->capt->channels = err; + } + loop->reinit = 0; + loop->use_samplerate = 0; +__again: + if (loop->latency_req) { + loop->latency_reqtime = frames_to_time(loop->play->rate_req, + loop->latency_req); + loop->latency_req = 0; + } + loop->latency = time_to_frames(loop->play->rate_req, loop->latency_reqtime); + if ((err = setparams(loop, loop->latency/2)) < 0) + goto __error; + if (verbose) + showlatency(loop->output, loop->latency, loop->play->rate_req, "Latency"); + if (loop->play->access == loop->capt->access && + loop->play->format == loop->capt->format && + loop->play->rate == loop->capt->rate && + loop->play->channels == loop->capt->channels && + loop->sync != SYNC_TYPE_SAMPLERATE) { + if (verbose > 1) + snd_output_printf(loop->output, "shared buffer!!!\n"); + if ((err = init_handle(loop->play, 1)) < 0) + goto __error; + if ((err = init_handle(loop->capt, 0)) < 0) + goto __error; + if (loop->play->buf_size < loop->capt->buf_size) { + char *nbuf = realloc(loop->play->buf, + loop->capt->buf_size * + loop->capt->frame_size); + if (nbuf == NULL) { + err = -ENOMEM; + goto __error; + } + loop->play->buf = nbuf; + loop->play->buf_size = loop->capt->buf_size; + } else if (loop->capt->buf_size < loop->play->buf_size) { + char *nbuf = realloc(loop->capt->buf, + loop->play->buf_size * + loop->play->frame_size); + if (nbuf == NULL) { + err = -ENOMEM; + goto __error; + } + loop->capt->buf = nbuf; + loop->capt->buf_size = loop->play->buf_size; + } + loop->capt->buf = loop->play->buf; + } else { + if ((err = init_handle(loop->play, 1)) < 0) + goto __error; + if ((err = init_handle(loop->capt, 1)) < 0) + goto __error; + if (loop->play->rate_req != loop->play->rate || + loop->capt->rate_req != loop->capt->rate) { + snd_pcm_format_t format1, format2; + loop->use_samplerate = 1; + format1 = loop->play->format; + format2 = loop->capt->format; + fix_format(loop, 1); + if (loop->play->format != format1 || + loop->capt->format != format2) { + pcmjob_stop(loop); + goto __again; + } + } + } +#ifdef USE_SAMPLERATE + if (loop->sync == SYNC_TYPE_SAMPLERATE) + loop->use_samplerate = 1; + if (loop->use_samplerate && !loop->src_enable) { + logit(LOG_CRIT, "samplerate conversion required but disabled\n"); + loop->use_samplerate = 0; + err = -EIO; + goto __error; + } + if (loop->use_samplerate) { + if ((loop->capt->format != SND_PCM_FORMAT_S16 || + loop->play->format != SND_PCM_FORMAT_S16) && + (loop->capt->format != SND_PCM_FORMAT_S32 || + loop->play->format != SND_PCM_FORMAT_S32)) { + logit(LOG_CRIT, "samplerate conversion supports only %s or %s formats (play=%s, capt=%s)\n", snd_pcm_format_name(SND_PCM_FORMAT_S16), snd_pcm_format_name(SND_PCM_FORMAT_S32), snd_pcm_format_name(loop->play->format), snd_pcm_format_name(loop->capt->format)); + loop->use_samplerate = 0; + err = -EIO; + goto __error; + } + loop->src_state = src_new(loop->src_converter_type, + loop->play->channels, &err); + loop->src_data.data_in = calloc(1, sizeof(float)*loop->capt->channels*loop->capt->buf_size); + if (loop->src_data.data_in == NULL) { + err = -ENOMEM; + goto __error; + } + loop->src_data.data_out = calloc(1, sizeof(float)*loop->play->channels*loop->play->buf_size); + if (loop->src_data.data_out == NULL) { + err = -ENOMEM; + goto __error; + } + loop->src_data.src_ratio = (double)loop->play->rate / + (double)loop->capt->rate; + loop->src_data.end_of_input = 0; + loop->src_out_frames = 0; + } else { + loop->src_state = NULL; + } +#else + if (loop->sync == SYNC_TYPE_SAMPLERATE || loop->use_samplerate) { + logit(LOG_CRIT, "alsaloop is compiled without libsamplerate support\n"); + err = -EIO; + goto __error; + } +#endif + if (verbose) { + snd_output_printf(loop->output, "%s sync type: %s", loop->id, sync_types[loop->sync]); +#ifdef USE_SAMPLERATE + if (loop->sync == SYNC_TYPE_SAMPLERATE) + snd_output_printf(loop->output, " (%s)", src_types[loop->src_converter_type]); +#endif + snd_output_printf(loop->output, "\n"); + } + lhandle_start(loop->play); + lhandle_start(loop->capt); + if ((err = snd_pcm_format_set_silence(loop->play->format, + loop->play->buf, + loop->play->buf_size * loop->play->channels)) < 0) { + logit(LOG_CRIT, "%s: silence error\n", loop->id); + goto __error; + } + if (verbose > 4) + snd_output_printf(loop->output, "%s: capt->buffer_size = %li, play->buffer_size = %li\n", loop->id, loop->capt->buf_size, loop->play->buf_size); + loop->pitch = 1.0; + update_pitch(loop); + loop->pitch_delta = 1.0 / ((double)loop->capt->rate * 4); + loop->total_queued_count = 0; + loop->pitch_diff = 0; + count = get_whole_latency(loop) / loop->play->pitch; + loop->play->buf_count = count; + if (loop->play->buf == loop->capt->buf) + loop->capt->buf_pos = count; + err = writeit(loop->play); + if (verbose > 4) + snd_output_printf(loop->output, "%s: silence queued %i samples\n", loop->id, err); + if (count > loop->play->buffer_size) + count = loop->play->buffer_size; + if (err != (int)count) { + logit(LOG_CRIT, "%s: initial playback fill error (%i/%i/%u)\n", loop->id, err, (int)count, loop->play->buffer_size); + err = -EIO; + goto __error; + } + loop->running = 1; + loop->stop_pending = 0; + if (loop->xrun) { + getcurtimestamp(&loop->xrun_last_update); + loop->xrun_last_pdelay = XRUN_PROFILE_UNKNOWN; + loop->xrun_last_cdelay = XRUN_PROFILE_UNKNOWN; + loop->xrun_max_proctime = 0; + } + if ((err = snd_pcm_start(loop->capt->handle)) < 0) { + logit(LOG_CRIT, "pcm start %s error: %s\n", loop->capt->id, snd_strerror(err)); + goto __error; + } + if (!loop->linked) { + if ((err = snd_pcm_start(loop->play->handle)) < 0) { + logit(LOG_CRIT, "pcm start %s error: %s\n", loop->play->id, snd_strerror(err)); + goto __error; + } + } + return 0; + __error: + pcmjob_stop(loop); + return err; +} + +int pcmjob_stop(struct loopback *loop) +{ + int err; + + if (loop->running) { + if ((err = snd_pcm_drop(loop->capt->handle)) < 0) + logit(LOG_WARNING, "pcm drop %s error: %s\n", loop->capt->id, snd_strerror(err)); + if ((err = snd_pcm_drop(loop->play->handle)) < 0) + logit(LOG_WARNING, "pcm drop %s error: %s\n", loop->play->id, snd_strerror(err)); + if ((err = snd_pcm_hw_free(loop->capt->handle)) < 0) + logit(LOG_WARNING, "pcm hw_free %s error: %s\n", loop->capt->id, snd_strerror(err)); + if ((err = snd_pcm_hw_free(loop->play->handle)) < 0) + logit(LOG_WARNING, "pcm hw_free %s error: %s\n", loop->play->id, snd_strerror(err)); + loop->running = 0; + } + freeloop(loop); + return 0; +} + +int pcmjob_pollfds_init(struct loopback *loop, struct pollfd *fds) +{ + int err, idx = 0; + + if (loop->running) { + err = snd_pcm_poll_descriptors(loop->play->handle, fds + idx, loop->play->pollfd_count); + if (err < 0) + return err; + idx += loop->play->pollfd_count; + err = snd_pcm_poll_descriptors(loop->capt->handle, fds + idx, loop->capt->pollfd_count); + if (err < 0) + return err; + idx += loop->capt->pollfd_count; + } + if (loop->play->ctl_pollfd_count > 0 && + (loop->slave == SLAVE_TYPE_ON || loop->controls)) { + err = snd_ctl_poll_descriptors(loop->play->ctl, fds + idx, loop->play->ctl_pollfd_count); + if (err < 0) + return err; + idx += loop->play->ctl_pollfd_count; + } + if (loop->capt->ctl_pollfd_count > 0 && + (loop->slave == SLAVE_TYPE_ON || loop->controls)) { + err = snd_ctl_poll_descriptors(loop->capt->ctl, fds + idx, loop->capt->ctl_pollfd_count); + if (err < 0) + return err; + idx += loop->capt->ctl_pollfd_count; + } + loop->active_pollfd_count = idx; + return idx; +} + +static snd_pcm_sframes_t get_queued_playback_samples(struct loopback *loop) +{ + snd_pcm_sframes_t delay; + + if (snd_pcm_delay(loop->play->handle, &delay) < 0) + return 0; + loop->play->last_delay = delay; + delay += loop->play->buf_count; +#ifdef USE_SAMPLERATE + delay += loop->src_out_frames; +#endif + return delay; +} + +static snd_pcm_sframes_t get_queued_capture_samples(struct loopback *loop) +{ + snd_pcm_sframes_t delay; + int err; + + if ((err = snd_pcm_delay(loop->capt->handle, &delay)) < 0) + return 0; + loop->capt->last_delay = delay; + delay += loop->capt->buf_count; + return delay; +} + +static int ctl_event_check(snd_ctl_elem_value_t *val, snd_ctl_event_t *ev) +{ + snd_ctl_elem_id_t *id1, *id2; + snd_ctl_elem_id_alloca(&id1); + snd_ctl_elem_id_alloca(&id2); + snd_ctl_elem_value_get_id(val, id1); + snd_ctl_event_elem_get_id(ev, id2); + if (snd_ctl_event_elem_get_mask(ev) == SND_CTL_EVENT_MASK_REMOVE) + return 0; + if ((snd_ctl_event_elem_get_mask(ev) & SND_CTL_EVENT_MASK_VALUE) == 0) + return 0; + return control_id_match(id1, id2); +} + +static int handle_ctl_events(struct loopback_handle *lhandle, + unsigned short events ATTRIBUTE_UNUSED) +{ + struct loopback *loop = lhandle->loopback; + snd_ctl_event_t *ev; + int err, restart = 0; + + snd_ctl_event_alloca(&ev); + while ((err = snd_ctl_read(lhandle->ctl, ev)) != 0 && err != -EAGAIN) { + if (err < 0) + break; + if (snd_ctl_event_get_type(ev) != SND_CTL_EVENT_ELEM) + continue; + if (lhandle == loop->play) + goto __ctl_check; + if (verbose > 6) + snd_output_printf(loop->output, "%s: ctl event!!!! %s\n", lhandle->id, snd_ctl_event_elem_get_name(ev)); + if (ctl_event_check(lhandle->ctl_active, ev)) { + continue; + } else if (ctl_event_check(lhandle->ctl_format, ev)) { + err = get_format(lhandle); + if (lhandle->format != err) + restart = 1; + continue; + } else if (ctl_event_check(lhandle->ctl_rate, ev)) { + err = get_rate(lhandle); + if ((int)lhandle->rate != err) + restart = 1; + continue; + } else if (ctl_event_check(lhandle->ctl_channels, ev)) { + err = get_channels(lhandle); + if ((int)lhandle->channels != err) + restart = 1; + continue; + } + __ctl_check: + control_event(lhandle, ev); + } + err = get_active(lhandle); + if (verbose > 7) + snd_output_printf(loop->output, "%s: ctl event active %i\n", lhandle->id, err); + if (!err) { + if (lhandle->loopback->running) { + loop->stop_pending = 1; + loop->stop_count = 0; + } + } else { + loop->stop_pending = 0; + if (loop->running == 0) + restart = 1; + } + if (restart) { + pcmjob_stop(loop); + err = pcmjob_start(loop); + if (err < 0) + return err; + } + return 1; +} + +int pcmjob_pollfds_handle(struct loopback *loop, struct pollfd *fds) +{ + struct loopback_handle *play = loop->play; + struct loopback_handle *capt = loop->capt; + unsigned short prevents, crevents, events; + snd_pcm_uframes_t ccount, pcount; + int err, loopcount = 0, idx; + + if (verbose > 11) + snd_output_printf(loop->output, "%s: pollfds handle\n", loop->id); + if (verbose > 13 || loop->xrun) + getcurtimestamp(&loop->tstamp_start); + if (verbose > 12) { + snd_pcm_sframes_t pdelay, cdelay; + if ((err = snd_pcm_delay(play->handle, &pdelay)) < 0) + snd_output_printf(loop->output, "%s: delay error: %s / %li / %li\n", play->id, snd_strerror(err), play->buf_size, play->buf_count); + else + snd_output_printf(loop->output, "%s: delay %li / %li / %li\n", play->id, pdelay, play->buf_size, play->buf_count); + if ((err = snd_pcm_delay(capt->handle, &cdelay)) < 0) + snd_output_printf(loop->output, "%s: delay error: %s / %li / %li\n", capt->id, snd_strerror(err), capt->buf_size, capt->buf_count); + else + snd_output_printf(loop->output, "%s: delay %li / %li / %li\n", capt->id, cdelay, capt->buf_size, capt->buf_count); + } + idx = 0; + if (loop->running) { + err = snd_pcm_poll_descriptors_revents(play->handle, fds, + play->pollfd_count, + &prevents); + if (err < 0) + return err; + idx += play->pollfd_count; + err = snd_pcm_poll_descriptors_revents(capt->handle, fds + idx, + capt->pollfd_count, + &crevents); + if (err < 0) + return err; + idx += capt->pollfd_count; + if (loop->xrun) { + if (prevents || crevents) { + loop->xrun_last_wake = loop->xrun_last_wake0; + loop->xrun_last_wake0 = loop->tstamp_start; + } + loop->xrun_last_check = loop->xrun_last_check0; + loop->xrun_last_check0 = loop->tstamp_start; + } + } else { + prevents = crevents = 0; + } + if (play->ctl_pollfd_count > 0 && + (loop->slave == SLAVE_TYPE_ON || loop->controls)) { + err = snd_ctl_poll_descriptors_revents(play->ctl, fds + idx, + play->ctl_pollfd_count, + &events); + if (err < 0) + return err; + if (events) { + err = handle_ctl_events(play, events); + if (err == 1) + return 0; + if (err < 0) + return err; + } + idx += play->ctl_pollfd_count; + } + if (capt->ctl_pollfd_count > 0 && + (loop->slave == SLAVE_TYPE_ON || loop->controls)) { + err = snd_ctl_poll_descriptors_revents(capt->ctl, fds + idx, + capt->ctl_pollfd_count, + &events); + if (err < 0) + return err; + if (events) { + err = handle_ctl_events(capt, events); + if (err == 1) + return 0; + if (err < 0) + return err; + } + idx += capt->ctl_pollfd_count; + } + if (verbose > 9) + snd_output_printf(loop->output, "%s: prevents = 0x%x, crevents = 0x%x\n", loop->id, prevents, crevents); + if (!loop->running) + goto __pcm_end; + do { + ccount = readit(capt); + if (prevents != 0 && crevents == 0 && + ccount == 0 && loopcount == 0) { + if (play->stall > 20) { + play->stall = 0; + increase_playback_avail_min(play); + break; + } + play->stall++; + break; + } + if (ccount > 0) + play->stall = 0; + buf_add(loop, ccount); + if (capt->xrun_pending || loop->reinit) + break; + /* we read new samples, if we have a room in the playback + buffer, feed them there */ + pcount = writeit(play); + buf_remove(loop, pcount); + if (pcount > 0) + play->stall = 0; + if (play->xrun_pending || loop->reinit) + break; + loopcount++; + } while ((ccount > 0 || pcount > 0) && loopcount < 10); + if (play->xrun_pending || capt->xrun_pending) { + if ((err = xrun_sync(loop)) < 0) + return err; + } + if (loop->reinit) { + err = pcmjob_stop(loop); + if (err < 0) + return err; + err = pcmjob_start(loop); + if (err < 0) + return err; + } + if (loop->sync != SYNC_TYPE_NONE && + play->counter >= play->sync_point && + capt->counter >= play->sync_point) { + snd_pcm_sframes_t diff, lat = get_whole_latency(loop); + diff = ((double)(((double)play->total_queued * play->pitch) + + ((double)capt->total_queued * capt->pitch)) / + (double)loop->total_queued_count) - lat; + /* FIXME: this algorithm may be slightly better */ + if (verbose > 3) + snd_output_printf(loop->output, "%s: sync diff %li old diff %li\n", loop->id, diff, loop->pitch_diff); + if (diff > 0) { + if (diff == loop->pitch_diff) + loop->pitch += loop->pitch_delta; + else if (diff > loop->pitch_diff) + loop->pitch += loop->pitch_delta*2; + } else if (diff < 0) { + if (diff == loop->pitch_diff) + loop->pitch -= loop->pitch_delta; + else if (diff < loop->pitch_diff) + loop->pitch -= loop->pitch_delta*2; + } + loop->pitch_diff = diff; + if (loop->pitch_diff_min > diff) + loop->pitch_diff_min = diff; + if (loop->pitch_diff_max < diff) + loop->pitch_diff_max = diff; + update_pitch(loop); + play->counter -= play->sync_point; + capt->counter -= play->sync_point; + play->total_queued = 0; + capt->total_queued = 0; + loop->total_queued_count = 0; + } + if (loop->sync != SYNC_TYPE_NONE) { + snd_pcm_sframes_t pqueued, cqueued; + + /* Reduce cumulative error by interleaving playback vs capture reading order */ + if (loop->total_queued_count & 1) { + pqueued = get_queued_playback_samples(loop); + cqueued = get_queued_capture_samples(loop); + } else { + cqueued = get_queued_capture_samples(loop); + pqueued = get_queued_playback_samples(loop); + } + + if (verbose > 4) + snd_output_printf(loop->output, "%s: queued %li/%li samples\n", loop->id, pqueued, cqueued); + if (pqueued > 0) + play->total_queued += pqueued; + if (cqueued > 0) + capt->total_queued += cqueued; + if (pqueued > 0 || cqueued > 0) + loop->total_queued_count += 1; + } + if (verbose > 12) { + snd_pcm_sframes_t pdelay, cdelay; + if ((err = snd_pcm_delay(play->handle, &pdelay)) < 0) + snd_output_printf(loop->output, "%s: end delay error: %s / %li / %li\n", play->id, snd_strerror(err), play->buf_size, play->buf_count); + else + snd_output_printf(loop->output, "%s: end delay %li / %li / %li\n", play->id, pdelay, play->buf_size, play->buf_count); + if ((err = snd_pcm_delay(capt->handle, &cdelay)) < 0) + snd_output_printf(loop->output, "%s: end delay error: %s / %li / %li\n", capt->id, snd_strerror(err), capt->buf_size, capt->buf_count); + else + snd_output_printf(loop->output, "%s: end delay %li / %li / %li\n", capt->id, cdelay, capt->buf_size, capt->buf_count); + } + __pcm_end: + if (verbose > 13 || loop->xrun) { + long diff; + getcurtimestamp(&loop->tstamp_end); + diff = timediff(loop->tstamp_end, loop->tstamp_start); + if (verbose > 13) + snd_output_printf(loop->output, "%s: processing time %lius\n", loop->id, diff); + if (loop->xrun && loop->xrun_max_proctime < diff) + loop->xrun_max_proctime = diff; + } + return 0; +} + +#define OUT(args...) \ + snd_output_printf(loop->state, ##args) + +static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void show_handle(struct loopback_handle *lhandle, const char *id) +{ + struct loopback *loop = lhandle->loopback; + + OUT(" %s: %s:\n", id, lhandle->id); + OUT(" device = '%s', ctldev '%s'\n", lhandle->device, lhandle->ctldev); + OUT(" card_number = %i\n", lhandle->card_number); + if (!loop->running) + return; + OUT(" access = %s, format = %s, rate = %u, channels = %u\n", snd_pcm_access_name(lhandle->access), snd_pcm_format_name(lhandle->format), lhandle->rate, lhandle->channels); + OUT(" buffer_size = %u, period_size = %u, avail_min = %li\n", lhandle->buffer_size, lhandle->period_size, lhandle->avail_min); + OUT(" xrun_pending = %i\n", lhandle->xrun_pending); + OUT(" buf_size = %li, buf_pos = %li, buf_count = %li, buf_over = %li\n", lhandle->buf_size, lhandle->buf_pos, lhandle->buf_count, lhandle->buf_over); + OUT(" pitch = %.8f\n", lhandle->pitch); +} + +void pcmjob_state(struct loopback *loop) +{ + pthread_t self = pthread_self(); + pthread_mutex_lock(&state_mutex); + OUT("State dump for thread %p job %i: %s:\n", (void *)self, loop->thread, loop->id); + OUT(" running = %i\n", loop->running); + OUT(" sync = %i\n", loop->sync); + OUT(" slave = %i\n", loop->slave); + if (!loop->running) + goto __skip; + OUT(" pollfd_count = %i\n", loop->pollfd_count); + OUT(" pitch = %.8f, delta = %.8f, diff = %li, min = %li, max = %li\n", loop->pitch, loop->pitch_delta, loop->pitch_diff, loop->pitch_diff_min, loop->pitch_diff_max); + OUT(" use_samplerate = %i\n", loop->use_samplerate); + __skip: + show_handle(loop->play, "playback"); + show_handle(loop->capt, "capture"); + pthread_mutex_unlock(&state_mutex); +} diff --git a/alsaloop/test.sh b/alsaloop/test.sh new file mode 100755 index 0000000..fac72b9 --- /dev/null +++ b/alsaloop/test.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +#DBG="gdb --args " +#DBG="strace" +#DBG="valgrind --leak-check=full" +ARGS= +CFGFILE="/tmp/alsaloop.test.cfg" + +test1() { + echo "TEST1" + $DBG ./alsaloop -C hw:1,0 -P plughw:0,0 \ + --tlatency 50000 \ + --mixer "name='Master Playback Volume'@name='Master Playback Volume'" \ + --mixer "name='Master Playback Switch'@name='Master Playback Switch'" \ + --mixer "name='PCM Playback Volume'" \ + --ossmixer "Master@VOLUME" \ + --ossmixer "PCM@PCM" \ + $ARGS +} + +test2() { + echo "TEST2" +cat > $CFGFILE < $CFGFILE < $CFGFILE < +Select the soundcard to use, if you have more than one. Cards are +numbered from 0 (the default). + +.TP +\fI\-D, \-\-device\fP +Select the mixer device to control. + +.TP +\fI\-V, \-\-view\fP +Select the starting view mode, either \fIplayback\fP, \fIcapture\fP or \fIall\fP. + +.TP +\fI\-B, \-\-black-background\fP +Use the black background color. + +.TP +\fI\-g, \-\-no\-color\fP +Toggle the using of colors. + +.SH MIXER VIEWS + +The top-left corner of \fBalsamixer\fP shows some basic +information: the card name, the mixer chip name, the current view +mode and the currently selected mixer item. +When the mixer item is switched off, \fI[Off]\fP is displayed in its +name. + +Volume bars are located below the basic information area. You can +scroll left/right when all controls can't be put in a single screen. +The name of each control is shown in the bottom below the volume bars. +The currently selected item is drawn in red and/or emphasized. + +Each mixer control with volume capability shows a box and the current +volume filled in that box. The volume percentages are displayed below +the volume bar for left and right channels. For a mono control, only +one value is shown there. + +When a mixer control is turned off, \fIM\fP (mute) appears below the +volume bar. When it's turned on, \fIO\fP in green appears instead. +You can toggle the switch via \fIm\fP key. + +When a mixer control has capture capability, the capture flag appears +below the volume bar, too. When the capture is turned off, +\-\-\-\-\-\-\- is shown. \fICAPTURE\fP in red appears when the +capture switch is turned on. In addition, \fIL\fP and \fIR\fP letters +appear in left and right side to indicate that left and the right +channels are turned on. + +Some controls have the enumeration list, and don't show boxes but only +texts which indicate the currently active item. You can change the +item via up/down keys. + +.SH VIEW MODES +\fBalsamixer\fP has three view modes: playback, capture and all. +In the playback view, only the controls related with playback are shown. +Similarly, only the controls for capture (recording) are shown in the capture +view. The all view mode shows all controls. The current view mode is displayed +in the top-left position together with the mixer name, etc. + +The default view mode is the playback view. You can change it via +\fI-V\fP option. + +Each view mode can be switched via keyboard commands, too. +See the next section. + +.SH KEYBOARD COMMANDS +\fBalsamixer\fP recognizes the following keyboard commands to control the soundcard. +Commands shown here in upper case can also be given in lower case. +To be reminded of these keystrokes, hit the \fIh\fP key. + +.SS +General Controls + +The \fILeft\fP and \fIright arrow\fP keys are used to select the +channel (or device, depending on your preferred terminology). You can +also use \fIn\fP ("next") and \fIp\fP ("previous"). + +The \fIUp\fP and \fIDown Arrows\fP control the volume for the +currently selected device. You can also use \fI+\fP or \fI\-\fP for the +same purpose. Both the left and right signals are affected. For +independent left and right control, see below. + +The \fIB\fP or \fI=\fP key adjusts the balance of volumes on left and +right channels. + +\fIM\fP toggles muting for the current channel (both left and right). +If the hardware supports it, you can +mute left and right independently by using \fI,\fP (or \fI<\fP) and +\fI.\fP (or \fI>\fP) respectively. + +\fISPACE\fP enables recording for the current channel. If any other +channels have recording enabled, they will have their recording function +disabled first. This only works for valid input channels, of course. + +\fIL\fP re-draws the screen. + +.SS +View Mode Controls +Function keys are used to change view modes. +You can switch to the help mode and the proc info mode via \fIF1\fP and +\fIF2\fP keys, respectively. +On terminals that can't use function keys like gnome\-terminal, \fI?\fP and +\fI/\fP keys can be used alternatively for help and proc modes. + +\fIF3\fP, \fIF4\fP and \fIF5\fP keys are used to switch to playback, capture +and all view mode, respectively. \fITAB\fP key toggles the +current view mode circularly. + +.SS +Quick Volume Changes + +\fIPageUp\fP increases volume by 5. + +\fIPageDown\fP decreases volume by 5. + +\fIEnd\fP sets volume to 0. + +You can also control left & right levels for the current channel +independently, as follows: + +[\fIQ\fP | \fIW\fP | \fIE\fP ] -- turn UP [ left | both | right ] + +[\fIZ\fP | \fIX\fP | \fIC\fP ] -- turn DOWN [ left | both | right ] + +If the currently selected mixer channel is not a stereo channel, then +all UP keys will work like \fIW\fP, and all DOWN keys will work like \fIX\fP. + +The number keys from \fI0\fP to \fI9\fP are to change the absolute volume +quickly. They correspond to 0 to 90% volume. + +.SS +Selecting the Sound Card + +You can select another sound card by pressing the \fIF6\fP or \fIS\fP keys. +This will show a list of available sound cards to choose from, +and an entry to enter the mixer device name by hand. + +.SS +Exiting + +Quit the program with \fIALT Q\fP, or by hitting \fIESC\fP. +Please note that you might need to hit \fIESC\fP twice on some terminals +since it's regarded as a prefix key. + +.SH VOLUME MAPPING +In \fBalsamixer\fP, the volume is mapped to a value that is more natural +for a human ear. The mapping is designed so that the position in the +interval is proportional to the volume as a human ear would perceive +it, i.e. the position is the cubic root of the linear sample +multiplication factor. For controls with a small range (24 dB or +less), the mapping is linear in the dB values so that each step has +the same size visually. + +Only for controls without dB information, a linear mapping of the +hardware volume register values is used (this is the same algorithm as +used in the old \fBalsamixer\fP). + +.SH FILES + +Configuration is read from the following files: + +\fI$XDG_CONFIG_HOME\fP/alsamixer.rc + +\fI$HOME\fP/.config/alsamixer.rc + +.SH CONFIGURATION + +Comments start with '#'. + +.TP +\fBcolor\fP \fIelement\fP \fIforeground\fP \fIbackground\fP [\fIattribute...\fP] + +Valid values for \fIforeground\fP and \fIbackground\fP are: red, green, yellow, blue, magenta, cyan, white, black, none / default. + +Valid values for \fIattribute\fP are: bold, normal, reverse, underline, dim, italic, blink. + +See section \fBTHEME ELEMENTS\fP for a list of \fIelements\fP. + +.TP +\fBset\fP \fIoption\fP \fIvalue\fP + +\fBmouse_wheel_step\fP \fI\fP + +Set the mouse wheel step to \fI\fP + +\fBmouse_wheel_focuses_control\fP \fI0|1\fP + +If enabled (\fI1\fP), mixer controls can be changed by hovering over them and scrolling the mouse wheel. + +\fBbackground\fP \fIcolor\fP + +Set the default background color + +.TP +\fBbind\fP \fIkey_definition\fP \fIcommand\fP + +A \fIkey_definition\fP may be: + - a single character + - a combination with control: C-a + - a combination with alt: M-a + - a special keyname: Enter, Escape, PageUp, PageDown, Left, Down, Right, Up, Home, End, Backspace, F1 .. F12 + +See section \fBCOMMANDS\fP for a list of \fIcommands\fP. + +.SS COMMANDS + +.TP +\fItextbox_close\fP +Close textbox + +.TP +\fItextbox_up\fP / \fItextbox_down\fP +Scroll text up/down by one line + +.TP +\fItextbox_left\fP / \fItextbox_right\fP +Scroll text left/right by one column + +.TP +\fItextbox_page_up\fP / \fItextbox_page_down\fP +Scroll text up/down one page + +.TP +\fItextbox_page_left\fP / \fItextbox_page_right\fP +Scroll text left/right one page + +.TP +\fItextbox_top\fP / \fItextbox_bottom\fP +Go to top/bottom position of text + +.TP +\fImixer_balance_volumes\fP +Balance both channels + +.TP +\fImixer_close\fP +Close alsamixer application + +.TP +\fImixer_help\fP +Show help + +.TP +\fImixer_mode_playback\fP +Show playback controls + +.TP +\fImixer_mode_capture\fP +Show capture controls + +.TP +\fImixer_mode_all\fP +Show playback and capture controls + +.TP +\fImixer_mode_toggle\fP +Toggle between playback/capture/all controls + +.TP +\fImixer_refresh\fP +Refresh screen + +.TP +\fImixer_previous\fP / \fImixer_next\fP +Select previous/next mixer control + +.TP +\fImixer_control_focus_\fP +Focus control number \fI\fP. + +.TP +\fImixer_control_set_\fP[\fI_left\fP|\fI_right\fP] +Change volume of current control to \fI\fP percent. + +.TP +\fImixer_control_up_\fP[\fI_left\fP|\fI_right\fP] +Increase volume of current control by \fI\fP percent. + +.TP +\fImixer_control_down_\fP[\fI_left\fP|\fI_right\fP] +Decrease volume of current control by \fI\fP percent. + +.TP +\fImixer_toggle_mute\fP[\fI_left\fP|\fI_right\fP] +Toggle mute state of both/left/right channels + +.TP +\fImixer_toggle_capture\fP[\fI_left\fP|\fI_right\fP] +Toggle capture state of both/left/right channels + +.TP +\fImixer_select_card\fP +Show window for selecting sound card + +.TP +\fImixer_system_information\fP +Show system information + + +.SS THEME ELEMENTS + +.TP +\fImixer_frame\fP Color of the box around the mixer + +.TP +\fImixer_text\fP Color of text at the top of the mixer + +.TP +\fImixer_active\fP Color of \fIAlsaMixer\fP heading + +.TP +\fIctl_frame\fP Color of the frame around the mixer controls + +.TP +\fIctl_mute\fP Color indicating the mute state (\fIMM\fP) + +.TP +\fIctl_nomute\fP Color indicating the unmute state (\fI00\fP) + +.TP +\fIctl_capture\fP Color of \fICAPTURE\fP text + +.TP +\fIctl_nocapture\fP Colof of disabled capture label + +.TP +\fIctl_label\fP Color of label underneath mixer controls + +.TP +\fIctl_label_focus\fP Color of label underneath focused mixer control + +.TP +\fIctl_mark_focus\fP Color of \fI<>\fP marks beside focused mixer label + +.TP +\fIctl_bar_lo\fP Color of lower volume bar + +.TP +\fIctl_bar_mi\fP Color of middle volume bar + +.TP +\fIctl_bar_hi\fP Color of top volume bar + +.TP +\fIctl_inactive\fP Color for inactive control + +.TP +\fIctl_label_inactive\fP Color for inactive label + +.TP +\fIerrormsg\fP Color used for error message + +.TP +\fIinfomsg\fP Color used for information message + +.TP +\fItextbox\fP Color used for textbox (user input) + +.TP +\fItextfield\fP Color used for textbox + +.TP +\fImenu\fP Color used for menu + +.TP +\fImenu_selected\fP Color used for selected entry in menu + +.SH SEE ALSO +\fB +amixer(1), +aplay(1), +arecord(1) +\fP + +.SH AUTHOR +.B alsamixer +has been written by Tim Janik and +been further improved by Jaroslav Kysela +and Clemens Ladisch . + +This manual page was provided by Paul Winkler . diff --git a/alsamixer/alsamixer.rc.example b/alsamixer/alsamixer.rc.example new file mode 100644 index 0000000..f8594b9 --- /dev/null +++ b/alsamixer/alsamixer.rc.example @@ -0,0 +1,192 @@ +### alsamixer configuration file +# +# location: ~/alsamixer.rc or ~/.config/alsamixer.rc or ~/.alsamixer.rc + + +### color configuration +# color [ ...] +# +# color: +# default, black, red, green, yellow, blue, magenta, cyan, white +# +# attr: +# bold, reverse, standout, dim, underline, italic, normal, blink +# +# element: +# mixer_frame, mixer_text, mixer_active, ctl_frame, ctl_mute, ctl_nomute, +# ctl_capture, ctl_nocapture, ctl_label, ctl_label_focus, ctl_mark_focus, +# ctl_bar_lo, ctl_bar_mi, ctl_bar_hi, ctl_inactive, ctl_label_inactive, +# errormsg, infomsg, textbox, textfield, menu, menu_selected + +#color mixer_frame cyan default +#color mixer_text cyan default +#color mixer_active yellow default bold +#color ctl_frame cyan default bold +#color ctl_mute cyan default +#color ctl_nomute white green bold +#color ctl_capture red default bold +#color ctl_nocapture white default +#color ctl_label white blue bold +#color ctl_label_focus red blue bold +#color ctl_mark_focus red default bold +#color ctl_bar_lo green green bold +#color ctl_bar_mi white white bold +#color ctl_bar_hi red red bold +#color ctl_inactive white default +#color ctl_label_inactive white default reverse +#color errormsg white red bold +#color infomsg white blue bold +#color textbox white blue bold +#color textfield white default reverse +#color menu white blue bold +#color menu_selected white blue reverse + + +### key bindings +# bind [mixer] +# bind textbox +# +# mixer-cmd: +# help, close, refresh, select_card, system_information, mode_all, +# mode_capture, mode_playback, mode_toggle, balance_control, next[_], +# previous[_], control_focus_<[_left|_right]N>, +# control_up[_][_left|_right], control_down[_][_left|_right], +# control_set_[_left|_right], toggle_mute[_left|_right], +# toggle_capture[_left|_right] +# +# textbox-cmd: +# close, down, up, left, right, page_down, page_up, page_left, page_right, +# top, bottom +# +# key: a key definition may be: +# - a single character (A, a, Z, z, ...) +# - a character with control-modifier (^x, C-x, c-x, ...) +# - a character with meta/alt-modifier (M-x, m-x, A-x, a-x, ...) +# - a character with both modifiers (C-M-x, M-C-x, M-^x, ...) +# - a curses keyname, no modifiers allowed (KEY_HOME, Home, F1, F(1), ...) + +#bind Escape mixer close +#bind Cancel mixer close +#bind F10 mixer close +#bind F1 mixer help +#bind Help mixer help +#bind H mixer help +#bind h mixer help +#bind ? mixer help +#bind F2 mixer system_information +#bind / mixer system_information +#bind F3 mixer mode_playback +#bind F4 mixer mode_capture +#bind F5 mixer mode_all +#bind Tab mixer mode_toggle +#bind F6 mixer select_card +#bind S mixer select_card +#bind s mixer select_card +#bind Refresh mixer refresh +#bind ^L mixer refresh +#bind L mixer refresh +#bind l mixer refresh +#bind Left mixer previous +#bind P mixer previous +#bind p mixer previous +#bind Right mixer next +#bind N mixer next +#bind n mixer next +#bind PPage mixer control_up_5 +#bind NPage mixer control_down_5 +#bind LL mixer control_set_0 +#bind END mixer control_set_0 +#bind 0 mixer control_set_0 +#bind 1 mixer control_set_10 +#bind 2 mixer control_set_20 +#bind 3 mixer control_set_30 +#bind 4 mixer control_set_40 +#bind 5 mixer control_set_50 +#bind 6 mixer control_set_60 +#bind 7 mixer control_set_70 +#bind 8 mixer control_set_80 +#bind 9 mixer control_set_90 +#bind Up mixer control_up +#bind + mixer control_up +#bind K mixer control_up +#bind k mixer control_up +#bind W mixer control_up +#bind w mixer control_up +#bind Down mixer control_down +#bind - mixer control_down +#bind J mixer control_down +#bind j mixer control_down +#bind X mixer control_down +#bind x mixer control_down +#bind Q mixer control_up_left +#bind q mixer control_up_left +#bind Y mixer control_down_left +#bind y mixer control_down_left +#bind Z mixer control_down_left +#bind z mixer control_down_left +#bind E mixer control_up_right +#bind e mixer control_up_right +#bind C mixer control_down_right +#bind c mixer control_down_right +#bind M mixer toggle_mute +#bind m mixer toggle_mute +#bind < mixer toggle_mute_left +#bind , mixer toggle_mute_left +#bind > mixer toggle_mute_right +#bind . mixer toggle_mute_right +#bind Space mixer toggle_capture +#bind IC mixer toggle_capture_left +#bind ; mixer toggle_capture_left +#bind DC mixer toggle_capture_right +#bind ' mixer toggle_capture_right +#bind B mixer balance_control +#bind b mixer balance_control +#bind = mixer balance_control + +#bind Escape textbox close +#bind Cancel textbox close +#bind Enter textbox close +#bind Close textbox close +#bind Exit textbox close +#bind Down textbox down +#bind SF textbox down +#bind J textbox down +#bind j textbox down +#bind X textbox down +#bind x textbox down +#bind Up textbox up +#bind SR textbox up +#bind K textbox up +#bind k textbox up +#bind W textbox up +#bind w textbox up +#bind Left textbox left +#bind H textbox left +#bind h textbox left +#bind P textbox left +#bind p textbox left +#bind Right textbox right +#bind L textbox right +#bind l textbox right +#bind N textbox right +#bind n textbox right +#bind NPage textbox page_down +#bind SPace textbox page_down +#bind PPage textbox page_up +#bind Backspace textbox page_up +#bind B textbox page_up +#bind b textbox page_up +#bind Home textbox top +#bind Beg textbox top +#bind LL textbox bottom +#bind End textbox bottom +#bind Tab textbox page_right +#bind BTab textbox page_left + + +### misc configuration +# set mouse_wheel_step [0..100] +# set mouse_wheel_focuses_control [0|1] + +#set mouse_wheel_step 1 +#set mouse_wheel_focuses_control 1 diff --git a/alsamixer/bindings.c b/alsamixer/bindings.c new file mode 100644 index 0000000..4c8db18 --- /dev/null +++ b/alsamixer/bindings.c @@ -0,0 +1,132 @@ +#include "bindings.h" +#include "mixer_widget.h" + +#define CNTRL(C) (C%32) + +uint8_t textbox_bindings[KEY_MAX] = { + ['\n'] = CMD_TEXTBOX_CLOSE, + ['\r'] = CMD_TEXTBOX_CLOSE, + [27] = CMD_TEXTBOX_CLOSE, + [KEY_CANCEL] = CMD_TEXTBOX_CLOSE, + [KEY_ENTER] = CMD_TEXTBOX_CLOSE, + [KEY_CLOSE] = CMD_TEXTBOX_CLOSE, + [KEY_EXIT] = CMD_TEXTBOX_CLOSE, + [KEY_DOWN] = CMD_TEXTBOX_DOWN, + [KEY_SF] = CMD_TEXTBOX_DOWN, + ['J'] = CMD_TEXTBOX_DOWN, + ['j'] = CMD_TEXTBOX_DOWN, + ['X'] = CMD_TEXTBOX_DOWN, + ['x'] = CMD_TEXTBOX_DOWN, + [KEY_UP] = CMD_TEXTBOX_UP, + [KEY_SR] = CMD_TEXTBOX_UP, + ['K'] = CMD_TEXTBOX_UP, + ['k'] = CMD_TEXTBOX_UP, + ['W'] = CMD_TEXTBOX_UP, + ['w'] = CMD_TEXTBOX_UP, + [KEY_LEFT] = CMD_TEXTBOX_LEFT, + ['H'] = CMD_TEXTBOX_LEFT, + ['h'] = CMD_TEXTBOX_LEFT, + ['P'] = CMD_TEXTBOX_LEFT, + ['p'] = CMD_TEXTBOX_LEFT, + [KEY_RIGHT] = CMD_TEXTBOX_RIGHT, + ['L'] = CMD_TEXTBOX_RIGHT, + ['l'] = CMD_TEXTBOX_RIGHT, + ['N'] = CMD_TEXTBOX_RIGHT, + ['n'] = CMD_TEXTBOX_RIGHT, + [KEY_NPAGE] = CMD_TEXTBOX_PAGE_DOWN, + [' '] = CMD_TEXTBOX_PAGE_DOWN, + [KEY_PPAGE] = CMD_TEXTBOX_PAGE_UP, + [KEY_BACKSPACE] = CMD_TEXTBOX_PAGE_UP, + ['B'] = CMD_TEXTBOX_PAGE_UP, + ['b'] = CMD_TEXTBOX_PAGE_UP, + [KEY_HOME] = CMD_TEXTBOX_TOP, + [KEY_BEG] = CMD_TEXTBOX_TOP, + [KEY_LL] = CMD_TEXTBOX_BOTTOM, + [KEY_END] = CMD_TEXTBOX_BOTTOM, + ['\t'] = CMD_TEXTBOX_PAGE_RIGHT, + [KEY_BTAB] = CMD_TEXTBOX_PAGE_LEFT, +}; + +command_enum mixer_bindings[] = { + [27] = CMD_MIXER_CLOSE, + [KEY_CANCEL] = CMD_MIXER_CLOSE, + [KEY_F(10)] = CMD_MIXER_CLOSE, + [KEY_F(1)] = CMD_MIXER_HELP, + [KEY_HELP] = CMD_MIXER_HELP, + ['H'] = CMD_MIXER_HELP, + ['h'] = CMD_MIXER_HELP, + ['?'] = CMD_MIXER_HELP, + [KEY_F(2)] = CMD_MIXER_SYSTEM_INFORMATION, + ['/'] = CMD_MIXER_SYSTEM_INFORMATION, + [KEY_F(3)] = CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, VIEW_MODE_PLAYBACK), + [KEY_F(4)] = CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, VIEW_MODE_CAPTURE), + [KEY_F(5)] = CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, VIEW_MODE_ALL), + ['\t'] = CMD_MIXER_TOGGLE_VIEW_MODE, + [KEY_F(6)] = CMD_MIXER_SELECT_CARD, + ['S'] = CMD_MIXER_SELECT_CARD, + ['s'] = CMD_MIXER_SELECT_CARD, + [KEY_REFRESH] = CMD_MIXER_REFRESH, + [CNTRL('L')] = CMD_MIXER_REFRESH, + ['L'] = CMD_MIXER_REFRESH, + ['l'] = CMD_MIXER_REFRESH, + [KEY_LEFT] = CMD_WITH_ARG(CMD_MIXER_PREVIOUS, 1), + ['p'] = CMD_WITH_ARG(CMD_MIXER_PREVIOUS, 1), + ['P'] = CMD_WITH_ARG(CMD_MIXER_PREVIOUS, 1), + [KEY_RIGHT] = CMD_WITH_ARG(CMD_MIXER_NEXT, 1), + ['N'] = CMD_WITH_ARG(CMD_MIXER_NEXT, 1), + ['n'] = CMD_WITH_ARG(CMD_MIXER_NEXT, 1), + [KEY_PPAGE] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 5), + [KEY_NPAGE] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 5), + [KEY_LL] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 0), + [KEY_END] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 0), + ['0'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 0), + ['1'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 10), + ['2'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 20), + ['3'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 30), + ['4'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 40), + ['5'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 50), + ['6'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 60), + ['7'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 70), + ['8'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 80), + ['9'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 90), +#if 0 + [KEY_BEG] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 100), + [KEY_HELP] = CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT, 100), +#endif + [KEY_UP] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 1), + ['+'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 1), + ['K'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 1), + ['k'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 1), + ['W'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 1), + ['w'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 1), + [KEY_DOWN] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1), + ['-'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1), + ['J'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1), + ['j'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1), + ['X'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1), + ['x'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1), + ['Q'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP_LEFT, 1), + ['q'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP_LEFT, 1), + ['Y'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN_LEFT, 1), + ['y'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN_LEFT, 1), + ['Z'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN_LEFT, 1), + ['z'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN_LEFT, 1), + ['E'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP_RIGHT, 1), + ['e'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_UP_RIGHT, 1), + ['C'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN_RIGHT, 1), + ['c'] = CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN_RIGHT, 1), + ['M'] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, LEFT|RIGHT), + ['m'] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, LEFT|RIGHT), + ['<'] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, LEFT), + [','] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, LEFT), + ['>'] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, RIGHT), + ['.'] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, RIGHT), + [' '] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT|RIGHT), + [KEY_IC] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT), + [';'] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT), + [KEY_DC] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, RIGHT), + ['\''] = CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, RIGHT), + ['B'] = CMD_MIXER_BALANCE_CONTROL, + ['b'] = CMD_MIXER_BALANCE_CONTROL, + ['='] = CMD_MIXER_BALANCE_CONTROL, +}; diff --git a/alsamixer/bindings.h b/alsamixer/bindings.h new file mode 100644 index 0000000..fa2e8d3 --- /dev/null +++ b/alsamixer/bindings.h @@ -0,0 +1,81 @@ +#ifndef BINDINGS_H_INCLUDED +#define BINDINGS_H_INCLUDED + +#include CURSESINC +#include +#include + +/* Commands are stored in an uint16_t and may take an unsigned numeric argument. + * The command itself is stored in the lower 7 bits, the argument is stored + * in the higher 9 bits. + * + * The value `0` is used for no (unbound) command. */ + +typedef uint16_t command_enum; +extern command_enum mixer_bindings[KEY_MAX]; +/* No need for a 16bit type, since textbox commands don't take arguments */ +extern uint8_t textbox_bindings[KEY_MAX]; + +#define CMD_WITH_ARG(CMD, ARG) \ + ((CMD) + ((ARG) << 9)) + +#define CMD_GET_COMMAND(CMD) \ + ((CMD) & 0x1FF) + +#define CMD_GET_ARGUMENT(CMD) \ + ((CMD) >> 9) + +enum mixer_command { + // `CMD % 4` should produce the channel mask + CMD_MIXER_CONTROL_DOWN_LEFT = 1, + CMD_MIXER_CONTROL_DOWN_RIGHT, + CMD_MIXER_CONTROL_DOWN, + CMD_MIXER_CONTROL_UP_LEFT = 5, + CMD_MIXER_CONTROL_UP_RIGHT, + CMD_MIXER_CONTROL_UP, + CMD_MIXER_CONTROL_SET_PERCENT_LEFT = 9, + CMD_MIXER_CONTROL_SET_PERCENT_RIGHT, + CMD_MIXER_CONTROL_SET_PERCENT, + + // Keep those in the same order as displayed on screen + CMD_MIXER_HELP, + CMD_MIXER_SYSTEM_INFORMATION, + CMD_MIXER_SELECT_CARD, + CMD_MIXER_CLOSE, + + CMD_MIXER_TOGGLE_VIEW_MODE, + CMD_MIXER_SET_VIEW_MODE, + CMD_MIXER_PREVIOUS, + CMD_MIXER_NEXT, + CMD_MIXER_FOCUS_CONTROL, + CMD_MIXER_TOGGLE_MUTE, + CMD_MIXER_TOGGLE_CAPTURE, + CMD_MIXER_BALANCE_CONTROL, + CMD_MIXER_REFRESH, + + // Mouse + CMD_MIXER_MOUSE_CLICK_MUTE, + CMD_MIXER_MOUSE_CLICK_VOLUME_BAR, + CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM, +}; + +enum textbox_command { + /* Since these commands are also used by the menu widget we make use of + * the menu_driver() request constants. + * KEY_MAX is substracted so the value fits in 8 bits. */ + CMD_TEXTBOX___MIN_MENU_COMMAND = MIN_MENU_COMMAND - KEY_MAX, + CMD_TEXTBOX_TOP = REQ_FIRST_ITEM - KEY_MAX, + CMD_TEXTBOX_BOTTOM = REQ_LAST_ITEM - KEY_MAX, + CMD_TEXTBOX_LEFT = REQ_LEFT_ITEM - KEY_MAX, + CMD_TEXTBOX_RIGHT = REQ_RIGHT_ITEM - KEY_MAX, + CMD_TEXTBOX_UP = REQ_UP_ITEM - KEY_MAX, + CMD_TEXTBOX_DOWN = REQ_DOWN_ITEM - KEY_MAX, + CMD_TEXTBOX_PAGE_DOWN = REQ_SCR_DPAGE - KEY_MAX, + CMD_TEXTBOX_PAGE_UP = REQ_SCR_UPAGE - KEY_MAX, + CMD_TEXTBOX___MAX_MENU_COMMAND = MAX_MENU_COMMAND - KEY_MAX, + CMD_TEXTBOX_PAGE_LEFT, + CMD_TEXTBOX_PAGE_RIGHT, + CMD_TEXTBOX_CLOSE, +}; + +#endif diff --git a/alsamixer/card_select.c b/alsamixer/card_select.c new file mode 100644 index 0000000..aac7b45 --- /dev/null +++ b/alsamixer/card_select.c @@ -0,0 +1,201 @@ +/* + * card_select.c - select a card by list or device name + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include "gettext_curses.h" +#include "die.h" +#include "mem.h" +#include "utils.h" +#include "colors.h" +#include "widget.h" +#include "menu_widget.h" +#include "mixer_widget.h" +#include "device_name.h" +#include "card_select.h" + +struct card { + struct card *next; + char *indexstr; + char *name; + char *device_name; +}; + +static struct widget list_widget; +static struct card first_card; +static ITEM **items; +static MENU *menu; +static ITEM *initial_item; + +static void on_key_enter(void) +{ + ITEM *item = current_item(menu); + if (item) { + struct card *card = item_userptr(item); + if (card->device_name) { + if (select_card_by_name(card->device_name)) + list_widget.close(); + } else { + create_device_name_form(); + } + } +} + +static void on_handle_key(int key) +{ + switch (menu_widget_handle_key(menu, key)) { + case KEY_ENTER: + on_key_enter(); + break; + case KEY_CANCEL: + list_widget.close(); + break; + } +} + +static void create(void) +{ + menu_widget_create(&list_widget, menu, _("Sound Card")); +} + +void close_card_select_list(void) +{ + unsigned int i; + struct card *card, *next_card; + + unpost_menu(menu); + free_menu(menu); + for (i = 0; items[i]; ++i) + free_item(items[i]); + free(items); + for (card = first_card.next; card; card = next_card) { + next_card = card->next; + free(card->indexstr); + free(card->name); + free(card->device_name); + free(card); + } + widget_free(&list_widget); +} + +static struct widget list_widget = { + .handle_key = on_handle_key, + .window_size_changed = create, + .close = close_card_select_list, +}; + +static int get_cards(void) +{ + int count, number, err; + snd_ctl_t *ctl; + snd_ctl_card_info_t *info; + char buf[32]; + struct card *card, *prev_card; + + first_card.indexstr = "-"; + first_card.name = _("(default)"); + first_card.device_name = "default"; + count = 1; + + snd_ctl_card_info_alloca(&info); + prev_card = &first_card; + number = -1; + for (;;) { + err = snd_card_next(&number); + if (err < 0) + fatal_alsa_error(_("cannot enumerate sound cards"), err); + if (number < 0) + break; +#if defined(SND_LIB_VER) && SND_LIB_VER(1, 2, 5) <= SND_LIB_VERSION + sprintf(buf, "sysdefault:%d", number); +#else + sprintf(buf, "hw:%d", number); +#endif + err = snd_ctl_open(&ctl, buf, 0); + if (err < 0) + continue; + err = snd_ctl_card_info(ctl, info); + snd_ctl_close(ctl); + if (err < 0) + continue; + card = ccalloc(1, sizeof *card); + card->device_name = cstrdup(buf); + card->indexstr = cstrdup(buf + 3); + card->name = cstrdup(snd_ctl_card_info_get_name(info)); + prev_card->next = card; + prev_card = card; + ++count; + } + + card = ccalloc(1, sizeof *card); + card->indexstr = cstrdup(" "); + card->name = cstrdup(_("enter device name...")); + prev_card->next = card; + ++count; + + return count; +} + +static void create_list_items(int cards) +{ + int i; + struct card *card; + ITEM *item; + + initial_item = NULL; + items = ccalloc(cards + 1, sizeof(ITEM*)); + i = 0; + for (card = &first_card; card; card = card->next) { + item = new_item(card->indexstr, card->name); + if (!item) + fatal_error("cannot create menu item"); + set_item_userptr(item, card); + items[i++] = item; + if (!initial_item && + mixer_device_name && + (!card->device_name || + !strcmp(card->device_name, mixer_device_name))) + initial_item = item; + } + assert(i == cards); +} + +void create_card_select_list(void) +{ + int cards; + + cards = get_cards(); + create_list_items(cards); + + menu = new_menu(items); + if (!menu) + fatal_error("cannot create menu"); + set_menu_fore(menu, attrs.menu_selected); + set_menu_back(menu, attrs.menu); + set_menu_mark(menu, NULL); + if (initial_item) + set_current_item(menu, initial_item); + set_menu_spacing(menu, 2, 1, 1); + menu_opts_on(menu, O_SHOWDESC); + + create(); +} diff --git a/alsamixer/card_select.h b/alsamixer/card_select.h new file mode 100644 index 0000000..4ba15fc --- /dev/null +++ b/alsamixer/card_select.h @@ -0,0 +1,7 @@ +#ifndef CARD_SELECT_H_INCLUDED +#define CARD_SELECT_H_INCLUDED + +void create_card_select_list(void); +void close_card_select_list(void); + +#endif diff --git a/alsamixer/cli.c b/alsamixer/cli.c new file mode 100644 index 0000000..a39a4f5 --- /dev/null +++ b/alsamixer/cli.c @@ -0,0 +1,180 @@ +/* + * alsamixer - curses mixer for the ALSA project + * Copyright (c) 1998,1999 Tim Janik + * Jaroslav Kysela + * Copyright (c) 2009 Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include "gettext_curses.h" +#include "mixer_widget.h" +#include "mainloop.h" +#include "configparser.h" +#include "colors.h" + +static int black_background = 0; +static int use_color = 1; +static int use_mouse = 1; +static const char* config_file = CONFIG_DEFAULT; +static struct snd_mixer_selem_regopt selem_regopt = { + .ver = 1, + .abstract = SND_MIXER_SABSTRACT_NONE, + .device = "default", +}; + +static void show_help(void) +{ + puts(_("Usage: alsamixer [options]")); + puts(_("Useful options:\n" + " -h, --help this help\n" + " -c, --card=NUMBER sound card number or id\n" + " -D, --device=NAME mixer device name\n" + " -m, --mouse enable mouse\n" + " -M, --no-mouse disable mouse\n" + " -f, --config=FILE configuration file\n" + " -F, --no-config do not load configuration file\n" + " -V, --view=MODE starting view mode: playback/capture/all\n" + " -B, --black-background use black background color")); + puts(_("Debugging options:\n" + " -g, --no-color toggle using of colors\n" + " -a, --abstraction=NAME mixer abstraction level: none/basic")); +} + +static void parse_options(int argc, char *argv[]) +{ + static const char short_options[] = "hc:f:FD:mMV:Bga:"; + static const struct option long_options[] = { + { .name = "help", .val = 'h' }, + { .name = "card", .has_arg = 1, .val = 'c' }, + { .name = "config", .has_arg = 1, .val = 'f' }, + { .name = "no-config", .val = 'F' }, + { .name = "device", .has_arg = 1, .val = 'D' }, + { .name = "mouse", .val = 'm' }, + { .name = "no-mouse", .val = 'M' }, + { .name = "view", .has_arg = 1, .val = 'V' }, + { .name = "black-background", .val = 'B' }, + { .name = "no-color", .val = 'g' }, + { .name = "abstraction", .has_arg = 1, .val = 'a' }, + { 0 } + }; + int option; + int card_index; + static char name_buf[24]; + + while ((option = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (option) { + case '?': + case 'h': + show_help(); + exit(EXIT_SUCCESS); + case 'c': + card_index = snd_card_get_index(optarg); + if (card_index < 0) { + fprintf(stderr, _("invalid card index: %s\n"), optarg); + goto fail; + } +#if defined(SND_LIB_VER) && SND_LIB_VER(1, 2, 5) <= SND_LIB_VERSION + sprintf(name_buf, "sysdefault:%d", card_index); +#else + sprintf(name_buf, "hw:%d", card_index); +#endif + selem_regopt.device = name_buf; + break; + case 'D': + selem_regopt.device = optarg; + break; + case 'f': + config_file = optarg; + break; + case 'F': + config_file = NULL; + break; + case 'm': + use_mouse = 1; + break; + case 'M': + use_mouse = 0; + break; + case 'V': + if (*optarg == 'p' || *optarg == 'P') + view_mode = VIEW_MODE_PLAYBACK; + else if (*optarg == 'c' || *optarg == 'C') + view_mode = VIEW_MODE_CAPTURE; + else + view_mode = VIEW_MODE_ALL; + break; + case 'B': + black_background = 1; + break; + case 'g': + use_color = !use_color; + break; + case 'a': + if (!strcmp(optarg, "none")) + selem_regopt.abstract = SND_MIXER_SABSTRACT_NONE; + else if (!strcmp(optarg, "basic")) + selem_regopt.abstract = SND_MIXER_SABSTRACT_BASIC; + else { + fprintf(stderr, _("unknown abstraction level: %s\n"), optarg); + goto fail; + } + break; + default: + fprintf(stderr, _("unknown option: %c\n"), option); +fail: + fputs(_("try `alsamixer --help' for more information\n"), stderr); + exit(EXIT_FAILURE); + } + } +} + +int main(int argc, char *argv[]) +{ + if (!isatty(fileno(stdin))) + return 0; + + setlocale(LC_ALL, ""); +#ifdef ENABLE_NLS_IN_CURSES + textdomain(PACKAGE); +#endif + + parse_options(argc, argv); + + create_mixer_object(&selem_regopt); + + initialize_curses(use_color, use_mouse); + + if (config_file == CONFIG_DEFAULT) + parse_default_config_file(); + else if (config_file) + parse_config_file(config_file); + + if (black_background) + reinit_colors(COLOR_BLACK); + + create_mixer_widget(); + + mainloop(); + + app_shutdown(); + return 0; +} diff --git a/alsamixer/colors.c b/alsamixer/colors.c new file mode 100644 index 0000000..cca5233 --- /dev/null +++ b/alsamixer/colors.c @@ -0,0 +1,133 @@ +/* + * colors.c - color and attribute definitions + * Copyright (c) 1998,1999 Tim Janik + * Jaroslav Kysela + * Copyright (c) 2009 Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include CURSESINC +#include "colors.h" + +struct attributes attrs; +static short background_color = -1; + +int get_color_pair(short fg, short bg) +{ + static int color_pairs_defined = 0; + short i, pair_fg, pair_bg; + + for (i = 1; i <= color_pairs_defined; ++i) { + if (OK == pair_content(i, &pair_fg, &pair_bg)) + if (pair_fg == fg && pair_bg == bg) + return COLOR_PAIR(i); + } + + if (color_pairs_defined + 1 < COLOR_PAIRS) { + ++color_pairs_defined; + init_pair(color_pairs_defined, fg, bg); + return COLOR_PAIR(color_pairs_defined); + } + + return 0; +} + +void reinit_colors(short bg) +{ + if (bg == background_color) + return; + init_pair(1, COLOR_CYAN, bg); + init_pair(2, COLOR_YELLOW, bg); + init_pair(4, COLOR_RED, bg); + init_pair(5, COLOR_WHITE, bg); + background_color = bg; +} + +void init_colors(int use_color) +{ + if (!!has_colors() == !!use_color) { + start_color(); + use_default_colors(); + + get_color_pair(COLOR_CYAN, background_color); // COLOR_PAIR(1) + get_color_pair(COLOR_YELLOW, background_color); + get_color_pair(COLOR_WHITE, COLOR_GREEN); + get_color_pair(COLOR_RED, background_color); + get_color_pair(COLOR_WHITE, background_color); + get_color_pair(COLOR_WHITE, COLOR_BLUE); + get_color_pair(COLOR_RED, COLOR_BLUE); + get_color_pair(COLOR_GREEN, COLOR_GREEN); + get_color_pair(COLOR_WHITE, COLOR_RED); // COLOR_PAIR(9) +#ifdef TRICOLOR_VOLUME_BAR + get_color_pair(COLOR_WHITE, COLOR_WHITE); + get_color_pair(COLOR_RED, COLOR_RED); +#endif + + attrs = (struct attributes) { + .mixer_frame = COLOR_PAIR(1), + .mixer_text = COLOR_PAIR(1), + .mixer_active = A_BOLD | COLOR_PAIR(2), + .ctl_frame = A_BOLD | COLOR_PAIR(1), + .ctl_mute = COLOR_PAIR(1), + .ctl_nomute = A_BOLD | COLOR_PAIR(3), + .ctl_capture = A_BOLD | COLOR_PAIR(4), + .ctl_nocapture = COLOR_PAIR(5), + .ctl_label = A_BOLD | COLOR_PAIR(6), + .ctl_label_focus = A_BOLD | COLOR_PAIR(7), + .ctl_mark_focus = A_BOLD | COLOR_PAIR(4), + .ctl_bar_lo = A_BOLD | COLOR_PAIR(8), +#ifdef TRICOLOR_VOLUME_BAR + .ctl_bar_mi = A_BOLD | COLOR_PAIR(10), + .ctl_bar_hi = A_BOLD | COLOR_PAIR(11), +#endif + .ctl_inactive = COLOR_PAIR(5), + .ctl_label_inactive = A_REVERSE | COLOR_PAIR(5), + .errormsg = A_BOLD | COLOR_PAIR(9), + .infomsg = A_BOLD | COLOR_PAIR(6), + .textbox = A_BOLD | COLOR_PAIR(6), + .textfield = A_REVERSE | COLOR_PAIR(5), + .menu = A_BOLD | COLOR_PAIR(6), + .menu_selected = A_REVERSE | COLOR_PAIR(6) + }; + } else { + attrs = (struct attributes) { + .mixer_frame = A_NORMAL, + .mixer_text = A_NORMAL, + .mixer_active = A_BOLD, + .ctl_frame = A_BOLD, + .ctl_mute = A_NORMAL, + .ctl_nomute = A_BOLD, + .ctl_capture = A_BOLD, + .ctl_nocapture = A_NORMAL, + .ctl_label = A_REVERSE, + .ctl_label_focus = A_REVERSE | A_BOLD, + .ctl_mark_focus = A_BOLD, + .ctl_bar_lo = A_BOLD, +#ifdef TRICOLOR_VOLUME_BAR + .ctl_bar_mi = A_BOLD, + .ctl_bar_hi = A_BOLD, +#endif + .ctl_inactive = A_NORMAL, + .ctl_label_inactive = A_REVERSE, + .errormsg = A_STANDOUT, + .infomsg = A_NORMAL, + .textbox = A_NORMAL, + .textfield = A_REVERSE, + .menu = A_NORMAL, + .menu_selected = A_REVERSE, + }; + } +} diff --git a/alsamixer/colors.h b/alsamixer/colors.h new file mode 100644 index 0000000..ac404e9 --- /dev/null +++ b/alsamixer/colors.h @@ -0,0 +1,42 @@ +#ifndef COLORS_H_INCLUDED +#define COLORS_H_INCLUDED + +#define TRICOLOR_VOLUME_BAR + +struct attributes { + // Alphabetically sorted +#ifdef TRICOLOR_VOLUME_BAR + int ctl_bar_hi; +#endif + int ctl_bar_lo; +#ifdef TRICOLOR_VOLUME_BAR + int ctl_bar_mi; +#endif + int ctl_capture; + int ctl_frame; + int ctl_inactive; + int ctl_label; + int ctl_label_focus; + int ctl_label_inactive; + int ctl_mark_focus; + int ctl_mute; + int ctl_nocapture; + int ctl_nomute; + int errormsg; + int infomsg; + int menu; + int menu_selected; + int mixer_active; + int mixer_frame; + int mixer_text; + int textbox; + int textfield; +}; + +extern struct attributes attrs; + +void init_colors(int use_color); +void reinit_colors(short bg); +int get_color_pair(short fg, short bg); + +#endif diff --git a/alsamixer/configparser.c b/alsamixer/configparser.c new file mode 100644 index 0000000..68ac673 --- /dev/null +++ b/alsamixer/configparser.c @@ -0,0 +1,601 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include CURSESINC +#include "colors.h" +#include "gettext_curses.h" +#include "utils.h" +#include "curskey.h" +#include "bindings.h" +#include "mixer_widget.h" + +#define ERROR_CONFIG (-1) +#define ERROR_MISSING_ARGUMENTS (-2) +#define ERROR_TOO_MUCH_ARGUMENTS (-3) + +static const char *error_message; +static const char *error_cause; + +static int strlist_index(const char *haystack, unsigned int itemlen, const char *needle) { + unsigned int needle_len; + unsigned int pos; + const char *found; + + needle_len = strlen(needle); + if (needle_len <= itemlen && needle[needle_len - 1] != ' ') { + found = strstr(haystack, needle); + if (found) { + pos = (found - haystack); + if (pos % itemlen == 0 && (needle_len == itemlen || haystack[pos+needle_len] == ' ')) + return pos / itemlen; + } + } + + return -1; +} + +static int color_by_name(const char *name) { + return strlist_index( + "default" + "black " + "red " + "green " + "yellow " + "blue " + "magenta" + "cyan " + "white ", 7, name) - 1; +}; + +static int attr_by_name(const char *name) { + return (int[]) { + -1, + A_BOLD, + A_REVERSE, + A_STANDOUT, + A_DIM, + A_UNDERLINE, +#ifdef A_ITALIC + A_ITALIC, +#endif + A_NORMAL, + A_BLINK, + }[strlist_index( + "bold " + "reverse " + "standout " + "dim " + "underline" +#ifdef A_ITALIC + "italic " +#endif + "normal " + "blink ", 9, name) + 1]; +}; + +#define W_NUMBER (1U << 0) + +enum textbox_word { + TW_BOTTOM = (1U << 1), + TW_CLOSE = (1U << 2), + TW_DOWN = (1U << 3), + TW_LEFT = (1U << 4), + TW_PAGE = (1U << 5), + TW_RIGHT = (1U << 6), + TW_TOP = (1U << 7), + TW_UP = (1U << 8), +}; + +const char *textbox_words = + "bottom" + "close " + "down " + "left " + "page " + "right " + "top " + "up "; + +enum mixer_word { + MW_ALL = (1U << 1), + MW_BALANCE = (1U << 2), + MW_CAPTURE = (1U << 3), + MW_CARD = (1U << 4), + MW_CLOSE = (1U << 5), + MW_CONTROL = (1U << 6), + MW_DOWN = (1U << 7), + MW_FOCUS = (1U << 8), + MW_HELP = (1U << 9), + MW_INFORMATION = (1U << 10), + MW_LEFT = (1U << 11), + MW_MODE = (1U << 12), + MW_MUTE = (1U << 13), + MW_NEXT = (1U << 14), + MW_PLAYBACK = (1U << 15), + MW_PREVIOUS = (1U << 16), + MW_REFRESH = (1U << 17), + MW_RIGHT = (1U << 18), + MW_SELECT = (1U << 19), + MW_SET = (1U << 20), + MW_SYSTEM = (1U << 21), + MW_TOGGLE = (1U << 22), + MW_UP = (1U << 23), +}; + +const char *mixer_words = + "all " + "balance " + "capture " + "card " + "close " + "control " + "down " + "focus " + "help " + "information" + "left " + "mode " + "mute " + "next " + "playback " + "previous " + "refresh " + "right " + "select " + "set " + "system " + "toggle " + "up "; + +static unsigned int parse_words(const char *name, const char* wordlist, unsigned int itemlen, unsigned int *number) { + unsigned int words = 0; + unsigned int word; + int i; + char buf[16]; + char *endptr; + + while (*name) { + for (i = 0; i < (int)sizeof(buf) - 1; ++i) { + if (*name == '\0') + break; + if (*name == '_') { + ++name; + break; + } + buf[i] = *name; + ++name; + } + buf[i] = '\0'; + + if (buf[0] >= '0' && buf[0] <= '9') { + if (number) { + *number = strtoumax(buf, &endptr, 10); + if (*endptr != '\0') + return 0; + } + word = W_NUMBER; + } + else if ((i = strlist_index(wordlist, itemlen, buf)) >= 0) + word = i <= 30 ? (2U << i) : 0; + else + return 0; + + if (words & word) // no duplicate words + return 0; + words |= word; + } + + return words; +} + +static int textbox_command_by_name(const char *name) { + switch (parse_words(name, textbox_words, 6, NULL)) { + case TW_TOP: return CMD_TEXTBOX_TOP; + case TW_BOTTOM: return CMD_TEXTBOX_BOTTOM; + case TW_CLOSE: return CMD_TEXTBOX_CLOSE; + case TW_UP: return CMD_TEXTBOX_UP; + case TW_DOWN: return CMD_TEXTBOX_DOWN; + case TW_LEFT: return CMD_TEXTBOX_LEFT; + case TW_RIGHT: return CMD_TEXTBOX_RIGHT; + case TW_PAGE|TW_UP: return CMD_TEXTBOX_PAGE_UP; + case TW_PAGE|TW_DOWN: return CMD_TEXTBOX_PAGE_DOWN; + case TW_PAGE|TW_LEFT: return CMD_TEXTBOX_PAGE_LEFT; + case TW_PAGE|TW_RIGHT: return CMD_TEXTBOX_PAGE_RIGHT; + default: return 0; + } +} + +static int mixer_command_by_name(const char *name) { + unsigned int channel = 0; + unsigned int number = 1; // default numeric arg + unsigned int words = parse_words(name, mixer_words, 11, &number); + + switch (words) { + case MW_HELP: return CMD_MIXER_HELP; + case MW_CLOSE: return CMD_MIXER_CLOSE; + case MW_REFRESH: return CMD_MIXER_REFRESH; + case MW_SELECT|MW_CARD: return CMD_MIXER_SELECT_CARD; + case MW_SYSTEM|MW_INFORMATION: return CMD_MIXER_SYSTEM_INFORMATION; + case MW_MODE|MW_ALL: return CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, VIEW_MODE_ALL); + case MW_MODE|MW_CAPTURE: return CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, VIEW_MODE_CAPTURE); + case MW_MODE|MW_PLAYBACK: return CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, VIEW_MODE_PLAYBACK); + case MW_MODE|MW_TOGGLE: return CMD_MIXER_TOGGLE_VIEW_MODE; + case MW_CONTROL|MW_BALANCE: return CMD_MIXER_BALANCE_CONTROL; + case MW_NEXT: + case MW_NEXT|W_NUMBER: + case MW_PREVIOUS: + case MW_PREVIOUS|W_NUMBER: + return ((number < 1 || number > 511) ? 0 : + CMD_WITH_ARG((words & MW_NEXT + ? CMD_MIXER_NEXT + : CMD_MIXER_PREVIOUS), number)); + case MW_CONTROL|MW_FOCUS|W_NUMBER: + return ((number < 1 || number > 512) ? 0 : + CMD_WITH_ARG(CMD_MIXER_FOCUS_CONTROL, number - 1)); + } + + if (words & MW_LEFT) + channel |= LEFT; + if (words & MW_RIGHT) + channel |= RIGHT; + if (!channel) + channel = LEFT|RIGHT; + + switch (words & ~(MW_LEFT|MW_RIGHT)) { + case MW_CONTROL|MW_UP: + case MW_CONTROL|MW_UP|W_NUMBER: + case MW_CONTROL|MW_DOWN: + case MW_CONTROL|MW_DOWN|W_NUMBER: + return ((number < 1 || number > 100) ? 0 : + CMD_WITH_ARG((words & MW_UP + ? CMD_MIXER_CONTROL_UP_LEFT + : CMD_MIXER_CONTROL_DOWN_LEFT) + channel - 1, number)); + case MW_CONTROL|MW_SET|W_NUMBER: + return (number > 100 ? 0 : + CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT_LEFT + channel - 1, number)); + case MW_TOGGLE|MW_MUTE: + return CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, channel); + case MW_TOGGLE|MW_CAPTURE: + return CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, channel); + } + + return 0; +} + +static int* element_by_name(const char *name) { + int idx = strlist_index( +#ifdef TRICOLOR_VOLUME_BAR + "ctl_bar_hi " +#endif + "ctl_bar_lo " +#ifdef TRICOLOR_VOLUME_BAR + "ctl_bar_mi " +#endif + "ctl_capture " + "ctl_frame " + "ctl_inactive " + "ctl_label " + "ctl_label_focus " + "ctl_label_inactive" + "ctl_mark_focus " + "ctl_mute " + "ctl_nocapture " + "ctl_nomute " + "errormsg " + "infomsg " + "menu " + "menu_selected " + "mixer_active " + "mixer_frame " + "mixer_text " + "textbox " + "textfield ", 18, name); + + if (idx < 0) { +#ifndef TRICOLOR_VOLUME_BAR + if (strlist_index( + "ctl_bar_hi" + "ctl_bar_mi", 10, name) >= 0) + return &errno; // dummy element +#endif + return NULL; + } + + return &( ((int*) &attrs)[idx] ); +} + +static int cfg_bind(char **argv, unsigned int argc) { + const char *command_name; + command_enum command = 0; + unsigned int i; + int keys[3] = { -1, -1, -1 }; + union { + command_enum *mixer_bindings; + uint8_t *textbox_bindings; + } bind_to = { + .mixer_bindings = mixer_bindings + }; + + if (argc == 2) + command_name = argv[1]; + else if (argc == 3) { + command_name = argv[2]; + + if (! strcmp(argv[1], "textbox")) { + bind_to.textbox_bindings = textbox_bindings; + } + else if (! strcmp(argv[1], "mixer")) + ; // bind_to.mixer_bindings = mixer_bindings + else { + error_message = _("invalid widget"); + error_cause = argv[1]; + return ERROR_CONFIG; + } + } + else { + return (argc < 2 ? ERROR_MISSING_ARGUMENTS : ERROR_TOO_MUCH_ARGUMENTS); + } + + keys[0] = curskey_parse(argv[0]); + if (keys[0] < 0 || keys[0] >= (int)ARRAY_SIZE(mixer_bindings)) { + error_message = _("invalid key"); + error_cause = argv[0]; + return ERROR_CONFIG; + } + + if (keys[0] == KEY_ENTER || keys[0] == '\n' || keys[0] == '\r') { + keys[0] = KEY_ENTER; + keys[1] = '\n'; + keys[2] = '\r'; + } + + if (bind_to.textbox_bindings == textbox_bindings) + command = textbox_command_by_name(command_name); + else + command = mixer_command_by_name(command_name); + + if (!command) { + if (!strcmp(command_name, "none")) + ; // command = 0 + else { + error_message = _("invalid command"); + error_cause = command_name; + return ERROR_CONFIG; + } + } + + for (i = 0; i < ARRAY_SIZE(keys) && keys[i] != -1; ++i) { + if (bind_to.textbox_bindings == textbox_bindings) + bind_to.textbox_bindings[keys[i]] = command; + else + bind_to.mixer_bindings[keys[i]] = command; + } + + return 0; +} + +static int cfg_color(char **argv, unsigned int argc) +{ + short fg_color, bg_color; + unsigned int i; + int *element; + int attr; + + if (argc < 3) + return ERROR_MISSING_ARGUMENTS; + + if (NULL == (element = element_by_name(argv[0]))) { + error_message = _("unknown theme element"); + error_cause = argv[0]; + return ERROR_CONFIG; + } + + if (-2 == (fg_color = color_by_name(argv[1]))) { + error_message = _("unknown color"); + error_cause = argv[1]; + return ERROR_CONFIG; + } + + if (-2 == (bg_color = color_by_name(argv[2]))) { + error_message = _("unknown color"); + error_cause = argv[2]; + return ERROR_CONFIG; + } + + *element = get_color_pair(fg_color, bg_color); + + for (i = 3; i < argc; ++i) { + if (-1 == (attr = attr_by_name(argv[i]))) { + error_message = _("unknown color attribute"); + error_cause = argv[i]; + return ERROR_CONFIG; + } + else + *element |= attr; + } + return 0; +} + +static int cfg_set(char **argv, unsigned int argc) +{ + char *endptr; + + if (argc == 2) { + if (! strcmp(argv[0], "mouse_wheel_step")) { + mouse_wheel_step = strtoumax(argv[1], &endptr, 10); + if (mouse_wheel_step > 100 || *endptr != '\0') { + mouse_wheel_step = 1; + error_message = _("invalid value"); + error_cause = argv[1]; + return ERROR_CONFIG; + } + } + else if (! strcmp(argv[0], "mouse_wheel_focuses_control")) { + if ((argv[1][0] == '0' || argv[1][0] == '1') && argv[1][1] == '\0') + mouse_wheel_focuses_control = argv[1][0] - '0'; + else { + error_message = _("invalid value"); + error_cause = argv[1]; + return ERROR_CONFIG; + } + } + else if (!strcmp(argv[0], "background")) { + int bg_color = color_by_name(argv[1]); + if (bg_color == -2) { + error_message = _("unknown color"); + error_cause = argv[1]; + return ERROR_CONFIG; + } + reinit_colors(bg_color); + } + else { + error_message = _("unknown option"); + error_cause = argv[0]; + return ERROR_CONFIG; + } + } + else { + return (argc < 2 ? ERROR_MISSING_ARGUMENTS : ERROR_TOO_MUCH_ARGUMENTS); + } + + return 0; +} + +/* Split $line on whitespace, store it in $args, return the argument count. + * Return 0 for commented lines ('\s*#'). + * + * This will modify contents of $line. + */ +static unsigned int parse_line(char *line, char **args, unsigned int args_size) +{ + unsigned int count; + + for (count = 0; count < args_size; ++count) { + while (*line && isspace(*line)) + ++line; + + if (*line == '\0') + break; + + if (*line == '#' && count == 0) + break; + + args[count] = line; + + while (*line && !isspace(*line)) + ++line; + + if (*line != '\0') { + *line = '\0'; + ++line; + } + } + + return count; +} + +static int process_line(char *line) { + char *args[16]; + unsigned int argc = parse_line(line, args, ARRAY_SIZE(args)); + int ret = 0; + + if (argc >= 1) { + error_cause = NULL; + //error_message = _("unknown error"); + + if (argc >= ARRAY_SIZE(args)) + ret = ERROR_TOO_MUCH_ARGUMENTS; + else { + ret = strlist_index( + "bind " + "color" + "set ", 5, args[0]); + switch (ret) { + case 0: ret = cfg_bind(args + 1, argc - 1); break; + case 1: ret = cfg_color(args + 1, argc - 1); break; + case 2: ret = cfg_set(args + 1, argc - 1); break; + default: error_message = _("unknown command"); + } + } + + if (ret == ERROR_MISSING_ARGUMENTS) + error_message = _("missing arguments"); + else if (ret == ERROR_TOO_MUCH_ARGUMENTS) + error_message = _("too much arguments"); + } + + return ret; +} + +void parse_config_file(const char *file_name) +{ + char *buf; + unsigned int file_size; + unsigned int lineno; + unsigned int i; + char *line; + + endwin(); // print warnings to stderr + + buf = read_file(file_name, &file_size); + if (!buf) { + fprintf(stderr, "%s: %s\n", file_name, strerror(errno)); + return; + } + + curskey_init(); + curskey_define_meta_keys(128); + + lineno = 0; + line = buf; + for (i = 0; i < file_size; ++i) { + if (buf[i] == '\n') { + buf[i] = '\0'; + ++lineno; + if (process_line(line) < 0) { + if (error_cause) + fprintf(stderr, "%s:%d: %s: %s: %s\n", file_name, lineno, line, error_message, error_cause); + else + fprintf(stderr, "%s:%d: %s: %s\n", file_name, lineno, line, error_message); + } + line = &buf[i + 1]; + } + } + + free(buf); + curskey_destroy(); +} + +void parse_default_config_file() { + char file[4096]; + const char *home; + + home = getenv("XDG_CONFIG_HOME"); + if (home && *home) { + snprintf(file, sizeof(file), "%s/alsamixer.rc", home); + if (! access(file, F_OK)) + return parse_config_file(file); + } + + home = getenv("HOME"); + if (!home || !*home) { + struct passwd *pwd = getpwuid(getuid()); + if (pwd) + home = pwd->pw_dir; + } + + if (home && *home) { + snprintf(file, sizeof(file), "%s/.config/alsamixer.rc", home); + if (! access(file, F_OK)) + return parse_config_file(file); + + snprintf(file, sizeof(file), "%s/.alsamixer.rc", home); + if (! access(file, F_OK)) + return parse_config_file(file); + } +} diff --git a/alsamixer/configparser.h b/alsamixer/configparser.h new file mode 100644 index 0000000..43f5392 --- /dev/null +++ b/alsamixer/configparser.h @@ -0,0 +1,9 @@ +#ifndef CONFIGPARSER_H_INCLUDED +#define CONFIGPARSER_H_INCLUDED + +#define CONFIG_DEFAULT ((const char*) 1) + +void parse_config_file(const char *file); +void parse_default_config_file(); + +#endif diff --git a/alsamixer/curskey.c b/alsamixer/curskey.c new file mode 100644 index 0000000..5f39b4a --- /dev/null +++ b/alsamixer/curskey.c @@ -0,0 +1,274 @@ +#include +#include +#include +#include "curskey.h" +#include "utils.h" +#include "mem.h" + +struct curskey_key { + char *keyname; + int keycode; +}; + +static struct curskey_key *keynames; +static unsigned int keynames_count; + +unsigned int meta_keycode_start; +static uint64_t invalid_meta_char_mask[2]; + +// Names for non-printable/whitespace characters and aliases for existing keys +static const struct curskey_key keyname_aliases[] = { + // Sorted by `keyname` + { "DEL", KEY_DEL }, + { "DELETE", KEY_DC }, + { "ENTER", '\n' }, + { "ENTER", '\r' }, + { "ESCAPE", KEY_ESCAPE }, + { "INSERT", KEY_IC }, + { "PAGEDOWN", KEY_NPAGE }, + { "PAGEUP", KEY_PPAGE }, + { "SPACE", KEY_SPACE }, + { "TAB", KEY_TAB } +}; + +#define STARTSWITH_KEY(S) \ + ((name[0] == 'K' || name[0] == 'k') && \ + (name[1] == 'E' || name[1] == 'e') && \ + (name[2] == 'Y' || name[2] == 'y') && \ + (name[3] == '_')) + +#define IS_META(S) \ + ((S[0] == 'M' || S[0] == 'm' || S[0] == 'A' || S[0] == 'a') && S[1] == '-') + +static int curskey_key_cmp(const void *a, const void *b) { + return strcmp(((struct curskey_key*) a)->keyname, + ((struct curskey_key*) b)->keyname); +} + +static int curskey_find(const struct curskey_key *table, unsigned int size, const char *name) { + unsigned int start = 0; + unsigned int end = size; + unsigned int i; + int cmp; + + while (1) { + i = (start+end) / 2; + cmp = strcasecmp(name, table[i].keyname); + + if (cmp == 0) + return table[i].keycode; + else if (end == start + 1) + return ERR; + else if (cmp > 0) + start = i; + else + end = i; + } +} + +/* Translate the name of a ncurses KEY_ constant to its value. + * "KEY_DOWN" -> 258 + * + * Return ERR on failure. + */ +int curskey_keycode(const char *name) +{ + int i; + + if (! name) + return ERR; + + if (STARTSWITH_KEY(name)) + name += 4; + + if (name[0] == 'F' || name[0] == 'f') { + i = (name[1] == '(' ? 2 : 1); + + if (name[i] >= '0' && name[i] <= '9') { + i = atoi(name + i); + if (i >= 1 && i <= 63) + return KEY_F(i); + } + } + + i = curskey_find(keyname_aliases, ARRAY_SIZE(keyname_aliases), name); + if (i != ERR) + return i; + + return curskey_find(keynames, keynames_count, name); +} + +static void free_ncurses_keynames() { + if (keynames) { + while (keynames_count) + free(keynames[--keynames_count].keyname); + free(keynames); + keynames = NULL; + } +} + +/* Create the list of ncurses KEY_ constants and their names. + * Returns OK on success, ERR on failure. + */ +int create_ncurses_keynames() { + int key; + char *name; + + free_ncurses_keynames(); + keynames = ccalloc(sizeof(struct curskey_key), (KEY_MAX - KEY_MIN)); + + for (key = KEY_MIN; key != KEY_MAX; ++key) { + name = (char*) keyname(key); + + if (!name || !STARTSWITH_KEY(name)) + continue; + + name += 4; + if (name[0] == 'F' && name[1] == '(') + continue; // ignore KEY_F(1),... + + keynames[keynames_count].keycode = key; + keynames[keynames_count].keyname = cstrdup(name); + ++keynames_count; + } + + keynames = crealloc(keynames, keynames_count * sizeof(struct curskey_key)); + qsort(keynames, keynames_count, sizeof(struct curskey_key), curskey_key_cmp); + + return OK; +} + +/* Defines meta escape sequences in ncurses. + * + * Some combinations with meta/alt may not be available since they collide + * with the prefix of a pre-defined key. + * For example, keys F1 - F4 begin with "\eO", so ALT-O cannot be defined. + * + * Returns OK if meta keys are available, ERR otherwise. + */ +int curskey_define_meta_keys(unsigned int keycode_start) { +#ifdef NCURSES_VERSION + int ch; + int keycode; + int new_keycode = keycode_start; + char key_sequence[3] = "\e "; + + invalid_meta_char_mask[0] = 0; + invalid_meta_char_mask[1] = 0; + + for (ch = 0; ch <= CURSKEY_MAX_META_CHAR; ++ch) { + key_sequence[1] = ch; + keycode = key_defined(key_sequence); + if (! keycode) { + define_key(key_sequence, new_keycode); + } + else if (keycode == new_keycode) + ; + else + invalid_meta_char_mask[ch/65] |= (1UL << (ch % 64)); + + ++new_keycode; + } + + meta_keycode_start = keycode_start; + return OK; +#endif + return ERR; +} + +/* Return the keycode for a key with modifiers applied. + * + * Available modifiers are: + * - CURSKEY_MOD_META / CURSKEY_MOD_ALT + * - CURSKEY_MOD_CNTRL + * + * See also the macros curskey_meta_key(), curskey_cntrl_key(). + * + * Returns ERR if the modifiers cannot be applied to this key. + */ +int curskey_mod_key(int key, unsigned int modifiers) { + if (modifiers & CURSKEY_MOD_CNTRL) { + if ((key >= 'A' && key <= '_') || (key >= 'a' && key <= 'z') || key == ' ') + key = key % 32; + else + return ERR; + } + + if (modifiers & CURSKEY_MOD_META) { + if (meta_keycode_start && + (key >= 0 && key <= CURSKEY_MAX_META_CHAR) && + ! (invalid_meta_char_mask[key/65] & (1UL << (key % 64)))) { + key = meta_keycode_start + key; + } + else + return ERR; + } + + return key; +} + +/* Return the ncurses keycode for a key definition. + * + * Key definition may be: + * - Single character (a, z, ...) + * - Character with control-modifier (^x, C-x, c-x, ...) + * - Character with meta/alt-modifier (M-x, m-x, A-x, a-x, ...) + * - Character with both modifiers (C-M-x, M-C-x, M-^x, ...) + * - Curses keyname, no modifiers allowed (KEY_HOME, HOME, F1, F(1), ...) + * + * Returns ERR if either + * - The key definition is NULL or empty + * - The key could not be found ("KEY_FOO") + * - The key combination is invalid in general ("C-TAB", "C-RETURN") + * - The key is invalid because of compile time options (the + * `define_key()` function was not available.) + * - The key is invalid because it could not be defined by + * curskey_define_meta_keys() + */ +int curskey_parse(const char *def) { + int c; + unsigned int mod = 0; + + if (! def) + return ERR; + + for (;;) { + if (def[0] == '^' && def[1] != '\0') { + ++def; + mod |= CURSKEY_MOD_CNTRL; + } + else if ((def[0] == 'C' || def[0] == 'c') && def[1] == '-') { + def += 2; + mod |= CURSKEY_MOD_CNTRL; + } + else if (IS_META(def)) { + if (! meta_keycode_start) + return ERR; + def += 2; + mod |= CURSKEY_MOD_ALT; + } + else + break; + } + + if (*def == '\0') + return ERR; + else if (*(def+1) == '\0') + c = *def; + else + c = curskey_keycode(def); + + return curskey_mod_key(c, mod); +} + +/* Initialize curskey. + * Returns OK on success, ERR on failure. */ +int curskey_init() { + keypad(stdscr, TRUE); + return create_ncurses_keynames(); +} + +/* Destroy curskey. */ +void curskey_destroy() { + free_ncurses_keynames(); +} diff --git a/alsamixer/curskey.h b/alsamixer/curskey.h new file mode 100644 index 0000000..129a652 --- /dev/null +++ b/alsamixer/curskey.h @@ -0,0 +1,37 @@ +#ifndef CURSKEY_H_INCLUDED +#define CURSKEY_H_INCLUDED + +#include CURSESINC + +/* Additional KEY_ constants */ +#define KEY_SPACE ' ' +#define KEY_TAB '\t' +#define KEY_DEL 127 +#define KEY_ESCAPE 27 +#define KEY_INSERT KEY_IC +#define KEY_DELETE KEY_DC +#define KEY_PAGEUP KEY_PPAGE +#define KEY_PAGEDOWN KEY_NPAGE + +/* Modifiers */ +#define CURSKEY_MOD_CNTRL 1U +#define CURSKEY_MOD_META 2U +#define CURSKEY_MOD_ALT CURSKEY_MOD_META + +/* Defines the range of characters which should be "meta-able" */ +#define CURSKEY_MAX_META_CHAR 127 + +int curskey_init(); +void curskey_destroy(); +int curskey_define_meta_keys(unsigned int keycode_start); + +int curskey_parse(const char *keydef); +int curskey_mod_key(int key, unsigned int modifiers); + +#define curskey_meta_key(KEY) \ + curskey_mod_key(KEY, CURSKEY_MOD_META) + +#define curskey_cntrl_key(KEY) \ + curskey_mod_key(KEY, CURSKEY_MOD_CNTRL) + +#endif diff --git a/alsamixer/device_name.c b/alsamixer/device_name.c new file mode 100644 index 0000000..26b03d5 --- /dev/null +++ b/alsamixer/device_name.c @@ -0,0 +1,195 @@ +/* + * device_name_form.c - ask for sound control device name + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include CURSESINC +#include +#include "gettext_curses.h" +#include "die.h" +#include "mem.h" +#include "utils.h" +#include "colors.h" +#include "widget.h" +#include "mixer_widget.h" +#include "card_select.h" +#include "device_name.h" + +static struct widget form_widget; +static FIELD *fields[3]; +static FORM *form; + +static char *dup_current_name(void) +{ + int rows, cols, max, i; + char *s; + + if (form_driver(form, REQ_VALIDATION) == E_OK) { + dynamic_field_info(fields[1], &rows, &cols, &max); + s = ccalloc(1, cols + 1); + memcpy(s, field_buffer(fields[1], 0), cols); + for (i = strlen(s) - 1; i >= 0 && s[i] == ' '; --i) + s[i] = '\0'; + return s; + } else { + return cstrdup(""); + } +} + +static void on_key_enter(void) +{ + char *s; + bool ok; + + s = dup_current_name(); + ok = select_card_by_name(s); + free(s); + if (ok) { + form_widget.close(); + close_card_select_list(); + } +} + +static void on_form_key(int key) +{ + static const struct { + int key; + int request; + } key_map[] = { + { KEY_LEFT, REQ_PREV_CHAR }, + { KEY_RIGHT, REQ_NEXT_CHAR }, + { KEY_HOME, REQ_BEG_FIELD }, + { KEY_BACKSPACE, REQ_DEL_PREV }, + { 127, REQ_DEL_PREV }, + { KEY_DC, REQ_DEL_CHAR }, + { KEY_BEG, REQ_BEG_FIELD }, + { KEY_END, REQ_END_FIELD }, + }; + unsigned int i; + + if (key >= 32 && key < 256 && key != 127) { + form_driver(form, key); + return; + } + for (i = 0; i < ARRAY_SIZE(key_map); ++i) + if (key_map[i].key == key) { + form_driver(form, key_map[i].request); + break; + } +} + +static void on_handle_key(int key) +{ + switch (key) { + case 27: + case KEY_CANCEL: + form_widget.close(); + break; + case 10: + case 13: + case KEY_ENTER: + on_key_enter(); + break; + default: + on_form_key(key); + break; + } +} + +static bool create(void) +{ + const char *title; + + if (screen_lines < 6 || screen_cols < 36) { + form_widget.close(); + beep(); + return FALSE; + } + widget_init(&form_widget, + 6, 36, SCREEN_CENTER, SCREEN_CENTER, + attrs.textbox, WIDGET_BORDER | WIDGET_SUBWINDOW | WIDGET_CURSOR_VISIBLE); + title = _("Sound Card"); + mvwprintw(form_widget.window, 0, (36 - 2 - get_mbs_width(title)) / 2, " %s ", title); + + set_form_win(form, form_widget.window); + set_form_sub(form, form_widget.subwindow); + return TRUE; +} + +static void on_window_size_changed(void) +{ + form_driver(form, REQ_VALIDATION); /* save field value */ + unpost_form(form); + + if (!create()) + return; + + /* + * This call fails because ncurses does not allow changing options of + * the current field, and we cannot change the current field because + * there is only one. The only way to make this work would be to throw + * away and recreate all fields. + */ + field_opts_off(fields[1], O_BLANK); + + post_form(form); +} + +static void on_close(void) +{ + unpost_form(form); + free_form(form); + free_field(fields[0]); + free_field(fields[1]); + widget_free(&form_widget); +} + +static struct widget form_widget = { + .handle_key = on_handle_key, + .window_size_changed = on_window_size_changed, + .close = on_close, +}; + +void create_device_name_form(void) +{ + fields[0] = new_field(1, 32, 1, 1, 0, 0); + if (!fields[0]) + fatal_error("cannot create field"); + field_opts_off(fields[0], O_ACTIVE|O_EDIT); + set_field_fore(fields[0], attrs.textbox); + set_field_back(fields[0], attrs.textbox); + set_field_buffer(fields[0], 0, _("Device name:")); + + fields[1] = new_field(1, 32, 2, 1, 0, 0); + if (!fields[1]) + fatal_error("cannot create field"); + field_opts_off(fields[1], O_AUTOSKIP|O_NULLOK|O_STATIC); + set_field_fore(fields[1], attrs.textfield); + set_field_back(fields[1], attrs.textfield); + set_field_buffer(fields[1], 0, mixer_device_name); + + form = new_form(fields); + if (!form) + fatal_error("cannot create form"); + + if (!create()) + return; + + post_form(form); +} diff --git a/alsamixer/device_name.h b/alsamixer/device_name.h new file mode 100644 index 0000000..f4a1f3f --- /dev/null +++ b/alsamixer/device_name.h @@ -0,0 +1,6 @@ +#ifndef DEVICE_NAME_FORM_H_INCLUDED +#define DEVICE_NAME_FORM_H_INCLUDED + +void create_device_name_form(void); + +#endif diff --git a/alsamixer/die.c b/alsamixer/die.c new file mode 100644 index 0000000..899a501 --- /dev/null +++ b/alsamixer/die.c @@ -0,0 +1,39 @@ +/* + * die.c - error handlers + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include "gettext_curses.h" +#include "mainloop.h" +#include "die.h" + +void fatal_error(const char *msg) +{ + app_shutdown(); + fprintf(stderr, "%s\n", msg); + exit(EXIT_FAILURE); +} + +void fatal_alsa_error(const char *msg, int err) +{ + app_shutdown(); + fprintf(stderr, _("%s: %s\n"), msg, snd_strerror(err)); + exit(EXIT_FAILURE); +} diff --git a/alsamixer/die.h b/alsamixer/die.h new file mode 100644 index 0000000..39ef1c0 --- /dev/null +++ b/alsamixer/die.h @@ -0,0 +1,7 @@ +#ifndef DIE_H_INCLUDED +#define DIE_H_INCLUDED + +void fatal_error(const char *msg) __attribute__((__noreturn__)); +void fatal_alsa_error(const char *msg, int err) __attribute__((__noreturn__)); + +#endif diff --git a/alsamixer/go b/alsamixer/go new file mode 100755 index 0000000..c33c298 --- /dev/null +++ b/alsamixer/go @@ -0,0 +1,10 @@ +#!/bin/bash + +#GDB="gdb --args" +GDB="" + +ALSA_MIXER_SIMPLE_MODULES="$HOME/hg/alsa-lib/modules/mixer/simple/.libs" \ +ALSA_MIXER_SIMPLE="$HOME/hg/alsa-lib/src/conf/smixer.conf" \ +ALSA_MIXER_SIMPLE_MPYTHON="$HOME/hg/alsa-lib/modules/mixer/simple/python/main.py" \ +LD_PRELOAD="$HOME/hg/alsa-lib/src/.libs/libasound.so" \ +$GDB ./alsamixer "$@" diff --git a/alsamixer/mainloop.c b/alsamixer/mainloop.c new file mode 100644 index 0000000..7ca785b --- /dev/null +++ b/alsamixer/mainloop.c @@ -0,0 +1,148 @@ +/* + * mainloop.c - main loop + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include "mem.h" +#include "die.h" +#include "colors.h" +#include "widget.h" +#include "mixer_widget.h" +#include "mixer_display.h" +#include "mixer_controls.h" +#include "mainloop.h" + +static WINDOW *curses_initialized; + +static void black_hole_error_handler(const char *file ATTRIBUTE_UNUSED, + int line ATTRIBUTE_UNUSED, + const char *function ATTRIBUTE_UNUSED, + int err ATTRIBUTE_UNUSED, + const char *fmt ATTRIBUTE_UNUSED, ...) +{ +} + +void initialize_curses(bool use_color, bool use_mouse) +{ + curses_initialized = initscr(); + cbreak(); + noecho(); +#ifdef HAVE_CURSES_ESCDELAY + set_escdelay(100); +#endif + window_size_changed(); /* update screen_lines/cols */ + init_colors(use_color); + if (use_mouse) + mousemask(ALL_MOUSE_EVENTS, NULL); + + snd_lib_error_set_handler(black_hole_error_handler); +} + +void app_shutdown(void) +{ + if (curses_initialized) { + clear(); + refresh(); + curs_set(1); + endwin(); + } + mixer_shutdown(); +} + +void mainloop(void) +{ + struct pollfd *pollfds = NULL; + int nfds = 0, n; + const struct widget *active_widget; + unsigned short revents; + int key; + int err; + + for (;;) { + update_panels(); + doupdate(); + + active_widget = get_active_widget(); + if (!active_widget) + break; + + n = 1 + snd_mixer_poll_descriptors_count(mixer); + if (n != nfds) { + free(pollfds); + nfds = n; + pollfds = ccalloc(nfds, sizeof *pollfds); + pollfds[0].fd = fileno(stdin); + pollfds[0].events = POLLIN; + } + err = snd_mixer_poll_descriptors(mixer, &pollfds[1], nfds - 1); + if (err < 0) + fatal_alsa_error("cannot get poll descriptors", err); + n = poll(pollfds, nfds, -1); + if (n < 0) { + if (errno == EINTR) { + pollfds[0].revents = 0; + doupdate(); /* handle SIGWINCH */ + } else { + fatal_error("poll error"); + } + } + if (pollfds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) + break; + if (pollfds[0].revents & POLLIN) + --n; + if (n > 0) { + err = snd_mixer_poll_descriptors_revents(mixer, &pollfds[1], nfds - 1, &revents); + if (err < 0) + fatal_alsa_error("cannot get poll events", err); + if (revents & (POLLERR | POLLNVAL)) + close_mixer_device(); + else if (revents & POLLIN) + snd_mixer_handle_events(mixer); + } + key = wgetch(active_widget->window); + while (key != ERR) { +#ifdef KEY_RESIZE + if (key == KEY_RESIZE) + window_size_changed(); + else +#endif + active_widget->handle_key(key); + active_widget = get_active_widget(); + if (!active_widget) + break; + key = wgetch(active_widget->window); + } + if (!active_widget) + break; + if (controls_changed) { + controls_changed = FALSE; + create_controls(); + control_values_changed = FALSE; + display_controls(); + } else if (control_values_changed) { + control_values_changed = FALSE; + display_controls(); + } + } + free(pollfds); +} diff --git a/alsamixer/mainloop.h b/alsamixer/mainloop.h new file mode 100644 index 0000000..d777e0f --- /dev/null +++ b/alsamixer/mainloop.h @@ -0,0 +1,10 @@ +#ifndef MAINLOOP_H_INCLUDED +#define MAINLOOP_H_INCLUDED + +#include CURSESINC + +void initialize_curses(bool use_color, bool use_mouse); +void mainloop(void); +void app_shutdown(void); + +#endif diff --git a/alsamixer/mem.c b/alsamixer/mem.c new file mode 100644 index 0000000..b6e1df2 --- /dev/null +++ b/alsamixer/mem.c @@ -0,0 +1,67 @@ +/* + * mem.c - memory allocation checkers + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include "die.h" +#include "mem.h" + +static void check(void *p) +{ + if (!p) + fatal_error("out of memory"); +} + +void *ccalloc(size_t n, size_t size) +{ + void *mem = calloc(n, size); + if (n && size) + check(mem); + return mem; +} + +void *crealloc(void *ptr, size_t new_size) +{ + ptr = realloc(ptr, new_size); + if (new_size) + check(ptr); + return ptr; +} + +char *cstrdup(const char *s) +{ + char *str = strdup(s); + check(str); + return str; +} + +char *casprintf(const char *fmt, ...) +{ + va_list ap; + char *str; + + va_start(ap, fmt); + if (vasprintf(&str, fmt, ap) < 0) + check(NULL); + va_end(ap); + return str; +} diff --git a/alsamixer/mem.h b/alsamixer/mem.h new file mode 100644 index 0000000..d0e5f54 --- /dev/null +++ b/alsamixer/mem.h @@ -0,0 +1,11 @@ +#ifndef MEM_H_INCLUDED +#define MEM_H_INCLUDED + +#include + +void *ccalloc(size_t n, size_t size); +void *crealloc(void *ptr, size_t new_size); +char *cstrdup(const char *s); +char *casprintf(const char *fmt, ...) __attribute__((__format__(printf, 1, 2))); + +#endif diff --git a/alsamixer/menu_widget.c b/alsamixer/menu_widget.c new file mode 100644 index 0000000..86d46b1 --- /dev/null +++ b/alsamixer/menu_widget.c @@ -0,0 +1,92 @@ +#include "menu_widget.h" +#include "colors.h" +#include "utils.h" +#include "bindings.h" + +/* Returns + * - KEY_CANCEL: close is requested + * - KEY_ENTER: item is selected + * - -1: no action + */ +int menu_widget_handle_key(MENU *menu, int key) +{ + MEVENT m; + + switch (key) { + case 27: + case KEY_CANCEL: + case 'q': + case 'Q': + return KEY_CANCEL; + case '\n': + case '\r': + case KEY_ENTER: + return KEY_ENTER; + + case KEY_MOUSE: + switch (menu_driver(menu, KEY_MOUSE)) { + case E_UNKNOWN_COMMAND: + /* If you double-click an item a REQ_TOGGLE_ITEM is generated + * and E_UNKNOWN_COMMAND is returned. (man menu_driver) */ + return KEY_ENTER; + case E_REQUEST_DENIED: + /* If menu did not handle KEY_MOUSE is has to be removed from + * input queue to prevent an infinite loop. */ + key = wgetch(menu_win(menu)); + if (key == KEY_MOUSE) { + if (getmouse(&m) == ERR) + return -1; + if (m.bstate & (BUTTON4_PRESSED|BUTTON4_CLICKED)) + menu_driver(menu, REQ_UP_ITEM); +#if NCURSES_MOUSE_VERSION > 1 + else if (m.bstate & (BUTTON5_PRESSED|BUTTON5_CLICKED)) + menu_driver(menu, REQ_DOWN_ITEM); +#endif + else + return KEY_CANCEL; + } + else if (key > 0) + ungetch(key); + } + return -1; + + default: + if (key < (int)ARRAY_SIZE(textbox_bindings)) { + key = textbox_bindings[key]; + if (key >= CMD_TEXTBOX___MIN_MENU_COMMAND && + key <= CMD_TEXTBOX___MAX_MENU_COMMAND) + menu_driver(menu, key + KEY_MAX); + } + + return -1; + } +} + +void menu_widget_create(struct widget *widget, MENU *menu, const char *title) +{ + int rows, columns; + + if (menu) + unpost_menu(menu); + + if (screen_lines < 3 || screen_cols < 10) { + widget->close(); + beep(); + return; + } + scale_menu(menu, &rows, &columns); + rows += 2; + columns += 2; + if (rows > screen_lines) + rows = screen_lines; + if (columns > screen_cols) + columns = screen_cols; + + widget_init(widget, rows, columns, SCREEN_CENTER, SCREEN_CENTER, + attrs.menu, WIDGET_BORDER | WIDGET_SUBWINDOW); + + mvwprintw(widget->window, 0, (columns - 2 - get_mbs_width(title)) / 2, " %s ", title); + set_menu_win(menu, widget->window); + set_menu_sub(menu, widget->subwindow); + post_menu(menu); +} diff --git a/alsamixer/menu_widget.h b/alsamixer/menu_widget.h new file mode 100644 index 0000000..e8e8a1e --- /dev/null +++ b/alsamixer/menu_widget.h @@ -0,0 +1,10 @@ +#ifndef MENU_WIDGET_H_INCLUDED +#define MENU_WIDGET_H_INCLUDED + +#include "widget.h" +#include + +int menu_widget_handle_key(MENU *menu, int key); +void menu_widget_create(struct widget *widget, MENU *menu, const char *title); + +#endif diff --git a/alsamixer/mixer_clickable.c b/alsamixer/mixer_clickable.c new file mode 100644 index 0000000..c772f37 --- /dev/null +++ b/alsamixer/mixer_clickable.c @@ -0,0 +1,136 @@ +#include +#include +#include "mixer_clickable.h" + +extern int screen_cols; +extern int screen_lines; + +static struct clickable_rect *clickable_rects = NULL; +static unsigned int clickable_rects_count = 0; +static unsigned int last_rect = 0; + +/* Using 0 instead of -1 for marking free rectangles allows us to use + * memset for `freeing` all rectangles at once. + * Zero is actually a valid coordinate in ncurses, but since we don't have + * any clickables in the top line this is fine. */ +#define FREE_MARKER 0 +#define RECT_IS_FREE(RECT) ((RECT).y1 == FREE_MARKER) +#define RECT_FREE(RECT) ((RECT).y1 = FREE_MARKER) + +void clickable_set(int y1, int x1, int y2, int x2, command_enum command, int arg1) { + struct clickable_rect* tmp; + unsigned int i; + + for (i = last_rect; i < clickable_rects_count; ++i) { + if (RECT_IS_FREE(clickable_rects[i])) { + last_rect = i; + goto SET_CLICKABLE_DATA; + } + } + + for (i = 0; i < last_rect; ++i) { + if (RECT_IS_FREE(clickable_rects[i])) { + last_rect = i; + goto SET_CLICKABLE_DATA; + } + } + + last_rect = clickable_rects_count; + tmp = realloc(clickable_rects, (clickable_rects_count + 8) * sizeof(*clickable_rects)); + if (!tmp) { + free(clickable_rects); + clickable_rects = NULL; + clickable_rects_count = 0; + last_rect = 0; + return; + } + clickable_rects = tmp; +#if FREE_MARKER == 0 + memset(clickable_rects + clickable_rects_count, 0, 8 * sizeof(*clickable_rects)); +#else + for (i = clickable_rects_count; i < clickable_rects_count + 8; ++i) + RECT_FREE(clickable_rects[i]); +#endif + clickable_rects_count += 8; + +SET_CLICKABLE_DATA: + clickable_rects[last_rect] = (struct clickable_rect) { + .y1 = y1, + .x1 = x1, + .x2 = x2, + .y2 = y2, + .command = command, + .arg1 = arg1 + }; +} + +void clickable_set_relative(WINDOW *win, int y1, int x1, int y2, int x2, command_enum command, int arg1) { + int y, x; + getyx(win, y, x); + y1 = y + y1; + x1 = x + x1; + y2 = y + y2; + x2 = x + x2; + clickable_set(y1, x1, y2, x2, command, arg1); +} + +void clickable_clear(int y1, int x1, int y2, int x2) { +#define IS_IN_RECT(Y, X) (Y >= y1 && Y <= y2 && X >= x1 && X <= x2) + unsigned int i; + + if (x1 == 0 && x2 == -1 && y2 == -1) { + if (y1 == 0) { + // Optimize case: clear all +#if FREE_MARKER == 0 + if (clickable_rects) + memset(clickable_rects, 0, + clickable_rects_count * sizeof(*clickable_rects)); +#else + for (i = 0; i < clickable_rects_count; ++i) + RECT_FREE(clickable_rects[i]); +#endif + } + else { + // Optimize case: clear all lines beyond y1 + for (i = 0; i < clickable_rects_count; ++i) { + if (clickable_rects[i].y2 >= y1) + RECT_FREE(clickable_rects[i]); + } + } + return; + } + + if (y2 < 0) + y2 = screen_lines + y2 + 1; + if (x2 < 0) + x2 = screen_cols + x2 + 1; + + for (i = 0; i < clickable_rects_count; ++i) { + if (!RECT_IS_FREE(clickable_rects[i]) && ( + IS_IN_RECT(clickable_rects[i].y1, clickable_rects[i].x1) || + IS_IN_RECT(clickable_rects[i].y2, clickable_rects[i].x2) + )) + { + RECT_FREE(clickable_rects[i]); + } + } +} + +struct clickable_rect* clickable_find(int y, int x) { + unsigned int i; + + for (i = 0; i < clickable_rects_count; ++i) { + if ( + !RECT_IS_FREE(clickable_rects[i]) && + y >= clickable_rects[i].y1 && + x >= clickable_rects[i].x1 && + y <= clickable_rects[i].y2 && + x <= clickable_rects[i].x2 + ) + { + return &clickable_rects[i]; + } + } + + return NULL; +} diff --git a/alsamixer/mixer_clickable.h b/alsamixer/mixer_clickable.h new file mode 100644 index 0000000..792c711 --- /dev/null +++ b/alsamixer/mixer_clickable.h @@ -0,0 +1,21 @@ +#ifndef MIXER_CLICKABLE_H +#define MIXER_CLICKABLE_H + +#include CURSESINC +#include "bindings.h" + +struct clickable_rect { + short y1; + short x1; + short y2; + short x2; + command_enum command; + int arg1; +}; + +void clickable_set(int y1, int x1, int y2, int x2, command_enum command, int arg1); +void clickable_set_relative(WINDOW *win, int y1, int x1, int y2, int x2, command_enum command, int arg1); +void clickable_clear(int y1, int x1, int y2, int x2); +struct clickable_rect* clickable_find(int y, int x); + +#endif diff --git a/alsamixer/mixer_controls.c b/alsamixer/mixer_controls.c new file mode 100644 index 0000000..9573517 --- /dev/null +++ b/alsamixer/mixer_controls.c @@ -0,0 +1,519 @@ +/* + * mixer_controls.c - handles mixer controls and mapping from selems + * Copyright (c) 1998,1999 Tim Janik + * Jaroslav Kysela + * Copyright (c) 2009 Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include CURSESINC +#include +#include "utils.h" +#include "mem.h" +#include "mixer_display.h" +#include "mixer_widget.h" +#include "mixer_controls.h" + +struct control *controls; +unsigned int controls_count; + +static const snd_mixer_selem_channel_id_t supported_channels[] = { + SND_MIXER_SCHN_FRONT_LEFT, + SND_MIXER_SCHN_FRONT_RIGHT, + SND_MIXER_SCHN_REAR_LEFT, + SND_MIXER_SCHN_REAR_RIGHT, + SND_MIXER_SCHN_FRONT_CENTER, + SND_MIXER_SCHN_WOOFER, + SND_MIXER_SCHN_SIDE_LEFT, + SND_MIXER_SCHN_SIDE_RIGHT, +}; +#define LAST_SUPPORTED_CHANNEL SND_MIXER_SCHN_SIDE_RIGHT + +static const snd_mixer_selem_channel_id_t control_channels[][2] = { + { SND_MIXER_SCHN_FRONT_LEFT, SND_MIXER_SCHN_FRONT_RIGHT }, + { SND_MIXER_SCHN_REAR_LEFT, SND_MIXER_SCHN_REAR_RIGHT }, + { SND_MIXER_SCHN_FRONT_CENTER, SND_MIXER_SCHN_UNKNOWN }, + { SND_MIXER_SCHN_WOOFER, SND_MIXER_SCHN_UNKNOWN }, + { SND_MIXER_SCHN_SIDE_LEFT, SND_MIXER_SCHN_SIDE_RIGHT }, +}; + +bool are_there_any_controls(void) +{ + snd_mixer_elem_t *elem; + unsigned int i; + + for (elem = snd_mixer_first_elem(mixer); + elem; + elem = snd_mixer_elem_next(elem)) { + if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_SIMPLE) + continue; + if (snd_mixer_selem_is_enumerated(elem)) + return TRUE; + if (snd_mixer_selem_has_playback_volume_joined(elem) || + snd_mixer_selem_has_capture_volume_joined(elem) || + snd_mixer_selem_has_playback_switch_joined(elem) || + snd_mixer_selem_has_capture_switch_joined(elem)) + return TRUE; + for (i = 0; i < ARRAY_SIZE(supported_channels); ++i) + if (snd_mixer_selem_has_playback_channel(elem, supported_channels[i]) || + snd_mixer_selem_has_capture_channel(elem, supported_channels[i])) + return TRUE; + } + return FALSE; +} + +static bool has_more_than_front_capture_channels(snd_mixer_elem_t *elem) +{ + unsigned int i; + + for (i = 2; i < ARRAY_SIZE(supported_channels); ++i) + if (snd_mixer_selem_has_capture_channel(elem, supported_channels[i])) + return TRUE; + return FALSE; +} + +static bool has_any_control_channel(snd_mixer_elem_t *elem, + const snd_mixer_selem_channel_id_t channels[2], + int (*has_channel)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t)) +{ + return has_channel(elem, channels[0]) || + (channels[1] != SND_MIXER_SCHN_UNKNOWN && has_channel(elem, channels[1])); +} + +static bool has_merged_cswitch(snd_mixer_elem_t *elem) +{ + bool pvol, psw; + unsigned int i; + + pvol = snd_mixer_selem_has_playback_volume(elem); + psw = snd_mixer_selem_has_playback_switch(elem); + if ((pvol || psw) && + snd_mixer_selem_has_capture_switch(elem) && + !snd_mixer_selem_has_capture_volume(elem)) { + if (snd_mixer_selem_has_capture_switch_joined(elem)) + return TRUE; + else if (((pvol && snd_mixer_selem_has_playback_volume_joined(elem)) || + (psw && snd_mixer_selem_has_playback_switch_joined(elem))) && + has_more_than_front_capture_channels(elem)) + return FALSE; + for (i = 0; i < ARRAY_SIZE(control_channels); ++i) { + if (has_any_control_channel(elem, control_channels[i], snd_mixer_selem_has_capture_channel) && + !has_any_control_channel(elem, control_channels[i], snd_mixer_selem_has_playback_channel)) + return FALSE; + } + return TRUE; + } + return FALSE; +} + +static unsigned int get_playback_controls_count(snd_mixer_elem_t *elem) +{ + unsigned int count = 0; + unsigned int i; + int has_vol, has_sw; + + has_vol = snd_mixer_selem_has_playback_volume(elem); + has_sw = snd_mixer_selem_has_playback_switch(elem); + if (!has_vol && !has_sw) + return 0; + if ((!has_vol || snd_mixer_selem_has_playback_volume_joined(elem)) && + (!has_sw || snd_mixer_selem_has_playback_switch_joined(elem))) + return 1; + for (i = 0; i < ARRAY_SIZE(control_channels); ++i) { + if (snd_mixer_selem_has_playback_channel(elem, control_channels[i][0]) || + (control_channels[i][1] != SND_MIXER_SCHN_UNKNOWN && + snd_mixer_selem_has_playback_channel(elem, control_channels[i][1]))) + ++count; + } + return count; +} + +static unsigned int get_capture_controls_count(snd_mixer_elem_t *elem) +{ + unsigned int count = 0; + unsigned int i; + int has_vol, has_sw; + + has_vol = snd_mixer_selem_has_capture_volume(elem); + has_sw = snd_mixer_selem_has_capture_switch(elem); + if ((!has_vol && !has_sw) || + (view_mode == VIEW_MODE_ALL && has_merged_cswitch(elem))) + return 0; + if ((!has_vol || snd_mixer_selem_has_capture_volume_joined(elem)) && + (!has_sw || snd_mixer_selem_has_capture_switch_joined(elem))) + return 1; + for (i = 0; i < ARRAY_SIZE(control_channels); ++i) { + if (snd_mixer_selem_has_capture_channel(elem, control_channels[i][0]) || + (control_channels[i][1] != SND_MIXER_SCHN_UNKNOWN && + snd_mixer_selem_has_capture_channel(elem, control_channels[i][1]))) + ++count; + } + return count; +} + +static unsigned int get_controls_count_for_elem(snd_mixer_elem_t *elem) +{ + unsigned int p, c; + + if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_SIMPLE) + return 0; + if (snd_mixer_selem_is_enumerated(elem)) { + switch (view_mode) { + case VIEW_MODE_PLAYBACK: + return snd_mixer_selem_is_enum_capture(elem) ? 0 : 1; + case VIEW_MODE_CAPTURE: + return snd_mixer_selem_is_enum_capture(elem) ? 1 : 0; + case VIEW_MODE_ALL: + default: + return 1; + } + } + switch (view_mode) { + case VIEW_MODE_PLAYBACK: + return get_playback_controls_count(elem); + case VIEW_MODE_CAPTURE: + return get_capture_controls_count(elem); + case VIEW_MODE_ALL: + default: + p = get_playback_controls_count(elem); + c = get_capture_controls_count(elem); + return has_merged_cswitch(elem) ? p : p + c; + } +} + +static void create_name(struct control *control) +{ + unsigned int index; + char *s; + + index = snd_mixer_selem_get_index(control->elem); + if (index > 0) + control->name = casprintf("%s %u", snd_mixer_selem_get_name(control->elem), index); + else + control->name = cstrdup(snd_mixer_selem_get_name(control->elem)); + + while ((s = strstr(control->name, "IEC958")) != NULL) + memcpy(s, "S/PDIF", 6); +} + +static unsigned int create_controls_for_elem(snd_mixer_elem_t *elem, struct control *control) +{ + unsigned int count = 0; + unsigned int i; + unsigned int enum_index; + struct control *front_control = NULL; + bool has_pvol, has_psw; + bool has_cvol, has_csw; + bool has_channel[LAST_SUPPORTED_CHANNEL + 1]; + bool merged_cswitch; + bool has_ch0, has_ch1; + + if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_SIMPLE) + return 0; + if (snd_mixer_selem_is_enumerated(elem)) { + if ((view_mode == VIEW_MODE_PLAYBACK && snd_mixer_selem_is_enum_capture(elem)) || + (view_mode == VIEW_MODE_CAPTURE && !snd_mixer_selem_is_enum_capture(elem))) + return 0; + control->elem = elem; + control->flags = TYPE_ENUM; + control->enum_channel_bits = 0; + for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i) + if (snd_mixer_selem_get_enum_item(control->elem, (snd_mixer_selem_channel_id_t)i, &enum_index) >= 0) + control->enum_channel_bits |= 1 << i; + if (snd_mixer_selem_is_active(control->elem)) + control->flags |= IS_ACTIVE; + create_name(control); + return 1; + } + has_pvol = snd_mixer_selem_has_playback_volume(elem); + has_psw = snd_mixer_selem_has_playback_switch(elem); + has_cvol = snd_mixer_selem_has_capture_volume(elem); + has_csw = snd_mixer_selem_has_capture_switch(elem); + merged_cswitch = view_mode == VIEW_MODE_ALL && has_merged_cswitch(elem); + if (view_mode != VIEW_MODE_CAPTURE && (has_pvol || has_psw)) { + if ((!has_pvol || snd_mixer_selem_has_playback_volume_joined(elem)) && + (!has_psw || snd_mixer_selem_has_playback_switch_joined(elem))) { + control->elem = elem; + if (has_pvol) { + control->flags |= TYPE_PVOLUME | HAS_VOLUME_0; + control->volume_channels[0] = 0; + } + if (has_psw) { + control->flags |= TYPE_PSWITCH | HAS_PSWITCH_0; + control->pswitch_channels[0] = 0; + } + if (merged_cswitch) { + control->flags |= TYPE_CSWITCH; + if (snd_mixer_selem_has_capture_switch_joined(elem)) { + control->flags |= HAS_CSWITCH_0; + control->cswitch_channels[0] = 0; + } else { + if (snd_mixer_selem_has_capture_channel(elem, control_channels[0][0])) { + control->flags |= HAS_CSWITCH_0; + control->cswitch_channels[0] = control_channels[0][0]; + } + if (control_channels[0][1] != SND_MIXER_SCHN_UNKNOWN && + snd_mixer_selem_has_capture_channel(elem, control_channels[0][1])) { + control->flags |= HAS_CSWITCH_1; + control->cswitch_channels[1] = control_channels[0][1]; + } + } + if ((control->flags & (HAS_CSWITCH_0 | HAS_CSWITCH_1)) == HAS_CSWITCH_1) { + control->flags ^= HAS_CSWITCH_0 | HAS_CSWITCH_1; + control->cswitch_channels[0] = control->cswitch_channels[1]; + } + } + if (snd_mixer_selem_is_active(control->elem)) + control->flags |= IS_ACTIVE; + create_name(control); + ++control; + ++count; + } else { + for (i = 0; i < ARRAY_SIZE(supported_channels); ++i) + has_channel[supported_channels[i]] = + snd_mixer_selem_has_playback_channel(elem, supported_channels[i]); + for (i = 0; i < ARRAY_SIZE(control_channels); ++i) { + has_ch0 = has_channel[control_channels[i][0]]; + has_ch1 = control_channels[i][1] != SND_MIXER_SCHN_UNKNOWN && + has_channel[control_channels[i][1]]; + if (!has_ch0 && !has_ch1) + continue; + control->elem = elem; + if (has_pvol) { + control->flags |= TYPE_PVOLUME; + if (snd_mixer_selem_has_playback_volume_joined(elem)) { + control->flags |= HAS_VOLUME_0; + control->volume_channels[0] = 0; + } else { + if (has_ch0) { + control->flags |= HAS_VOLUME_0; + control->volume_channels[0] = control_channels[i][0]; + } + if (has_ch1) { + control->flags |= HAS_VOLUME_1; + control->volume_channels[1] = control_channels[i][1]; + } + } + } + if (has_psw) { + control->flags |= TYPE_PSWITCH; + if (snd_mixer_selem_has_playback_switch_joined(elem)) { + control->flags |= HAS_PSWITCH_0; + control->pswitch_channels[0] = 0; + } else { + if (has_ch0) { + control->flags |= HAS_PSWITCH_0; + control->pswitch_channels[0] = control_channels[i][0]; + } + if (has_ch1) { + control->flags |= HAS_PSWITCH_1; + control->pswitch_channels[1] = control_channels[i][1]; + } + } + } + if (merged_cswitch) { + control->flags |= TYPE_CSWITCH; + if (snd_mixer_selem_has_capture_switch_joined(elem)) { + control->flags |= HAS_CSWITCH_0; + control->cswitch_channels[0] = 0; + } else { + if (snd_mixer_selem_has_capture_channel(elem, control_channels[i][0])) { + control->flags |= HAS_CSWITCH_0; + control->cswitch_channels[0] = control_channels[i][0]; + } + if (control_channels[i][1] != SND_MIXER_SCHN_UNKNOWN && + snd_mixer_selem_has_capture_channel(elem, control_channels[i][1])) { + control->flags |= HAS_CSWITCH_1; + control->cswitch_channels[1] = control_channels[i][1]; + } + } + } + if ((control->flags & (HAS_VOLUME_0 | HAS_VOLUME_1)) == HAS_VOLUME_1) { + control->flags ^= HAS_VOLUME_0 | HAS_VOLUME_1; + control->volume_channels[0] = control->volume_channels[1]; + } + if ((control->flags & (HAS_PSWITCH_0 | HAS_PSWITCH_1)) == HAS_PSWITCH_1) { + control->flags ^= HAS_PSWITCH_0 | HAS_PSWITCH_1; + control->pswitch_channels[0] = control->pswitch_channels[1]; + } + if ((control->flags & (HAS_CSWITCH_0 | HAS_CSWITCH_1)) == HAS_CSWITCH_1) { + control->flags ^= HAS_CSWITCH_0 | HAS_CSWITCH_1; + control->cswitch_channels[0] = control->cswitch_channels[1]; + } + if (snd_mixer_selem_is_active(control->elem)) + control->flags |= IS_ACTIVE; + create_name(control); + if (i == 0) + front_control = control; + else { + front_control->flags |= IS_MULTICH | 0; + control->flags |= IS_MULTICH | i; + } + ++control; + ++count; + } + } + } + if (view_mode != VIEW_MODE_PLAYBACK && (has_cvol || has_csw) && !merged_cswitch) { + if ((!has_cvol || snd_mixer_selem_has_capture_volume_joined(elem)) && + (!has_csw || snd_mixer_selem_has_capture_switch_joined(elem))) { + control->elem = elem; + if (has_cvol) { + control->flags |= TYPE_CVOLUME | HAS_VOLUME_0; + control->volume_channels[0] = 0; + } + if (has_csw) { + control->flags |= TYPE_CSWITCH | HAS_CSWITCH_0; + control->cswitch_channels[0] = 0; + } + if (snd_mixer_selem_is_active(control->elem)) + control->flags |= IS_ACTIVE; + create_name(control); + ++control; + ++count; + } else { + for (i = 0; i < ARRAY_SIZE(supported_channels); ++i) + has_channel[supported_channels[i]] = + snd_mixer_selem_has_capture_channel(elem, supported_channels[i]); + for (i = 0; i < ARRAY_SIZE(control_channels); ++i) { + has_ch0 = has_channel[control_channels[i][0]]; + has_ch1 = control_channels[i][1] != SND_MIXER_SCHN_UNKNOWN && + has_channel[control_channels[i][1]]; + if (!has_ch0 && !has_ch1) + continue; + control->elem = elem; + if (has_cvol) { + control->flags |= TYPE_CVOLUME; + if (snd_mixer_selem_has_capture_volume_joined(elem)) { + control->flags |= HAS_VOLUME_0; + control->volume_channels[0] = 0; + } else { + if (has_ch0) { + control->flags |= HAS_VOLUME_0; + control->volume_channels[0] = control_channels[i][0]; + } + if (has_ch1) { + control->flags |= HAS_VOLUME_1; + control->volume_channels[1] = control_channels[i][1]; + } + } + } + if (has_csw) { + control->flags |= TYPE_CSWITCH; + if (snd_mixer_selem_has_capture_switch_joined(elem)) { + control->flags |= HAS_CSWITCH_0; + control->cswitch_channels[0] = 0; + } else { + if (has_ch0) { + control->flags |= HAS_CSWITCH_0; + control->cswitch_channels[0] = control_channels[i][0]; + } + if (has_ch1) { + control->flags |= HAS_CSWITCH_1; + control->cswitch_channels[1] = control_channels[i][1]; + } + } + } + if ((control->flags & (HAS_VOLUME_0 | HAS_VOLUME_1)) == HAS_VOLUME_1) { + control->flags ^= HAS_VOLUME_0 | HAS_VOLUME_1; + control->volume_channels[0] = control->volume_channels[1]; + } + if ((control->flags & (HAS_CSWITCH_0 | HAS_CSWITCH_1)) == HAS_CSWITCH_1) { + control->flags ^= HAS_CSWITCH_0 | HAS_CSWITCH_1; + control->cswitch_channels[0] = control->cswitch_channels[1]; + } + if (snd_mixer_selem_is_active(control->elem)) + control->flags |= IS_ACTIVE; + create_name(control); + if (i == 0) + front_control = control; + else { + front_control->flags |= IS_MULTICH | 0; + control->flags |= IS_MULTICH | i; + } + ++control; + ++count; + } + } + } + return count; +} + +static void search_for_focus_control(void) +{ + snd_mixer_elem_t *elem; + unsigned int i; + + elem = snd_mixer_find_selem(mixer, current_selem_id); + if (elem) + for (i = 0; i < controls_count; ++i) + if (controls[i].elem == elem) { + focus_control_index = i; + for (;;) { + ++i; + if (i >= controls_count || controls[i].elem != elem) + return; + if (controls[i].flags == current_control_flags) { + focus_control_index = i; + return; + } + } + } + focus_control_index = 0; +} + +void free_controls(void) +{ + unsigned int i; + + for (i = 0; i < controls_count; ++i) + free(controls[i].name); + free(controls); + controls = NULL; + controls_count = 0; +} + +void create_controls(void) +{ + snd_mixer_elem_t *elem; + struct control *control; + + free_controls(); + + for (elem = snd_mixer_first_elem(mixer); + elem; + elem = snd_mixer_elem_next(elem)) + controls_count += get_controls_count_for_elem(elem); + + if (controls_count > 0) { + controls = ccalloc(controls_count, sizeof *controls); + control = controls; + for (elem = snd_mixer_first_elem(mixer); + elem; + elem = snd_mixer_elem_next(elem)) + control += create_controls_for_elem(elem, control); + assert(control == controls + controls_count); + } + + compute_controls_layout(); + display_view_mode(); + + search_for_focus_control(); + refocus_control(); +} diff --git a/alsamixer/mixer_controls.h b/alsamixer/mixer_controls.h new file mode 100644 index 0000000..dbb3a9d --- /dev/null +++ b/alsamixer/mixer_controls.h @@ -0,0 +1,37 @@ +#ifndef MIXER_CONTROLS_H_INCLUDED +#define MIXER_CONTROLS_H_INCLUDED + +#include + +struct control { + snd_mixer_elem_t *elem; + char *name; + unsigned int flags; +#define TYPE_PVOLUME (1u << 4) +#define TYPE_CVOLUME (1u << 5) +#define TYPE_PSWITCH (1u << 6) +#define TYPE_CSWITCH (1u << 7) +#define TYPE_ENUM (1u << 8) +#define HAS_VOLUME_0 (1u << 9) +#define HAS_VOLUME_1 (1u << 10) +#define HAS_PSWITCH_0 (1u << 11) +#define HAS_PSWITCH_1 (1u << 12) +#define HAS_CSWITCH_0 (1u << 13) +#define HAS_CSWITCH_1 (1u << 14) +#define IS_MULTICH (1u << 15) +#define IS_ACTIVE (1u << 16) +#define MULTICH_MASK (0x0000f) + snd_mixer_selem_channel_id_t volume_channels[2]; + snd_mixer_selem_channel_id_t pswitch_channels[2]; + snd_mixer_selem_channel_id_t cswitch_channels[2]; + unsigned int enum_channel_bits; +}; + +extern struct control *controls; +extern unsigned int controls_count; + +bool are_there_any_controls(void); +void create_controls(void); +void free_controls(void); + +#endif diff --git a/alsamixer/mixer_display.c b/alsamixer/mixer_display.c new file mode 100644 index 0000000..697da77 --- /dev/null +++ b/alsamixer/mixer_display.c @@ -0,0 +1,763 @@ +/* + * mixer_display.c - handles displaying of mixer widget and controls + * Copyright (c) 1874 Lewis Carroll + * Copyright (c) 2009 Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define _C99_SOURCE /* lrint() */ +#include "aconfig.h" +#include +#include +#include +#include +#include CURSESINC +#include +#include "gettext_curses.h" +#include "utils.h" +#include "mem.h" +#include "colors.h" +#include "widget.h" +#include "volume_mapping.h" +#include "mixer_widget.h" +#include "mixer_controls.h" +#include "mixer_display.h" +#include "mixer_clickable.h" + +enum align { + ALIGN_LEFT, + ALIGN_RIGHT, + ALIGN_CENTER, +}; + +static bool screen_too_small; +static bool has_info_items; + +static int info_items_left; +static int info_items_width; + +static int visible_controls; +static int first_visible_control_index; +static int first_control_x; +static int control_width; +static int control_name_width; + +static int base_y; +static int volume_height; +static int cswitch_y; +static int values_y; +static int name_y; +static int channel_name_y; + +static void display_string_in_field(int y, int x, const char *s, int width, enum align align) +{ + int string_width; + const char *s_end; + int spaces; + int cur_y, cur_x; + + wmove(mixer_widget.window, y, x); + string_width = width; + s_end = mbs_at_width(s, &string_width, -1); + if (string_width >= width) { + waddnstr(mixer_widget.window, s, s_end - s); + } else { + if (align != ALIGN_LEFT) { + spaces = width - string_width; + if (align == ALIGN_CENTER) + spaces /= 2; + if (spaces > 0) + wprintw(mixer_widget.window, "%*s", spaces, ""); + } + waddstr(mixer_widget.window, s); + if (align != ALIGN_RIGHT) { + getyx(mixer_widget.window, cur_y, cur_x); + if (cur_y == y) { + spaces = x + width - cur_x; + if (spaces > 0) + wprintw(mixer_widget.window, "%*s", spaces, ""); + } + } + } +} + +void init_mixer_layout(void) +{ + const char *labels_left[4] = { + _("Card:"), + _("Chip:"), + _("View:"), + _("Item:"), + }; + const char *labels_right[4] = { + _("F1: Help"), + _("F2: System information"), + _("F6: Select sound card"), + _("Esc: Exit"), + }; + int label_width_left, label_width_right; + int right_x, i; + + clickable_clear(0, 0, -1, -1); + screen_too_small = screen_lines < 14 || screen_cols < 12; + has_info_items = screen_lines >= 6; + if (!has_info_items) + return; + + label_width_left = get_max_mbs_width(labels_left, 4); + label_width_right = get_max_mbs_width(labels_right, 4); + if (2 + label_width_left + 1 + 28 + label_width_right + 2 > screen_cols) + label_width_right = 0; + if (2 + label_width_left + 1 + 28 + label_width_right + 2 > screen_cols) + label_width_left = 0; + + info_items_left = label_width_left ? 3 + label_width_left : 2; + right_x = screen_cols - label_width_right - 2; + info_items_width = right_x - info_items_left; + if (info_items_width < 1) { + has_info_items = FALSE; + return; + } + + wattrset(mixer_widget.window, attrs.mixer_text); + if (label_width_left) + for (i = 0; i < 4; ++i) + display_string_in_field(1 + i, 2, labels_left[i], + label_width_left, ALIGN_RIGHT); + if (label_width_right) + for (i = 0; i < 4; ++i) { + display_string_in_field(1 + i, right_x, labels_right[i], + label_width_right, ALIGN_LEFT); + clickable_set(1 + i, right_x, 1 + i, right_x + label_width_right - 1, + CMD_MIXER_HELP + i, -1); + } +} + +void display_card_info(void) +{ + snd_hctl_t *hctl; + snd_ctl_t *ctl; + snd_ctl_card_info_t *card_info; + const char *card_name = NULL; + const char *mixer_name = NULL; + int err; + + if (!has_info_items) + return; + + snd_ctl_card_info_alloca(&card_info); + if (mixer_device_name) + err = snd_mixer_get_hctl(mixer, mixer_device_name, &hctl); + else + err = -1; + if (err >= 0) { + ctl = snd_hctl_ctl(hctl); + err = snd_ctl_card_info(ctl, card_info); + if (err >= 0) { + card_name = snd_ctl_card_info_get_name(card_info); + mixer_name = snd_ctl_card_info_get_mixername(card_info); + } + } + + if (card_name) + wattrset(mixer_widget.window, attrs.mixer_active); + else { + wattrset(mixer_widget.window, attrs.mixer_text); + if (unplugged) + card_name = _("(unplugged)"); + else + card_name = "-"; + } + display_string_in_field(1, info_items_left, card_name, info_items_width, ALIGN_LEFT); + + if (mixer_name) + wattrset(mixer_widget.window, attrs.mixer_active); + else { + wattrset(mixer_widget.window, attrs.mixer_text); + mixer_name = "-"; + } + display_string_in_field(2, info_items_left, mixer_name, info_items_width, ALIGN_LEFT); +} + +void display_view_mode(void) +{ + const char *modes[3] = { + _("Playback"), + _("Capture"), + _("All"), + }; + int widths[3]; + bool has_view_mode; + int i; + + clickable_clear(3, 0, 3, 30); + if (!has_info_items) + return; + + has_view_mode = controls_count > 0 || are_there_any_controls(); + for (i = 0; i < 3; ++i) + widths[i] = get_mbs_width(modes[i]); + if (4 + widths[0] + 6 + widths[1] + 6 + widths[2] + 1 <= info_items_width) { + wmove(mixer_widget.window, 3, info_items_left - 1); + wattrset(mixer_widget.window, attrs.mixer_text); + for (i = 0; i < 3; ++i) { + wprintw(mixer_widget.window, " F%c:", '3' + i); + if (has_view_mode && (int)view_mode == i) { + wattrset(mixer_widget.window, attrs.mixer_active); + wprintw(mixer_widget.window, "[%s]", modes[i]); + wattrset(mixer_widget.window, attrs.mixer_text); + } else { + wprintw(mixer_widget.window, " %s ", modes[i]); + } + clickable_set_relative(mixer_widget.window, 0, -(widths[i] + 5), 0, -1, + CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, i), -1); + } + } else { + wattrset(mixer_widget.window, attrs.mixer_active); + display_string_in_field(3, info_items_left, + has_view_mode ? modes[view_mode] : "", + info_items_width, ALIGN_LEFT); + } +} + +static char *format_gain(long db) +{ + if (db != SND_CTL_TLV_DB_GAIN_MUTE) + return casprintf("%.2f", db / 100.0); + else + return cstrdup(_("mute")); +} + +static void display_focus_item_info(void) +{ + struct control *control; + unsigned int index; + char buf[64]; + long db, db2; + int sw, sw2; + char *dbs, *dbs2; + char *value_info; + char *item_info; + int err; + + if (!has_info_items) + return; + wattrset(mixer_widget.window, attrs.mixer_active); + if (!controls_count || screen_too_small) { + display_string_in_field(4, info_items_left, "", info_items_width, ALIGN_LEFT); + return; + } + control = &controls[focus_control_index]; + value_info = NULL; + if (control->flags & TYPE_ENUM) { + err = snd_mixer_selem_get_enum_item(control->elem, ffs(control->enum_channel_bits) - 1, &index); + if (err >= 0) + err = snd_mixer_selem_get_enum_item_name(control->elem, index, sizeof buf - 1, buf); + if (err >= 0) + value_info = casprintf(" [%s]", buf); + } else if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) { + int (*get_vol_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *); + + if (control->flags & TYPE_PVOLUME) + get_vol_func = snd_mixer_selem_get_playback_dB; + else + get_vol_func = snd_mixer_selem_get_capture_dB; + if (!(control->flags & HAS_VOLUME_1)) { + err = get_vol_func(control->elem, control->volume_channels[0], &db); + if (err >= 0) { + dbs = format_gain(db); + value_info = casprintf(" [%s %s]", _("dB gain:"), dbs); + free(dbs); + } + } else { + err = get_vol_func(control->elem, control->volume_channels[0], &db); + if (err >= 0) + err = get_vol_func(control->elem, control->volume_channels[1], &db2); + if (err >= 0) { + dbs = format_gain(db); + dbs2 = format_gain(db2); + value_info = casprintf(_(" [%s %s, %s]"), _("dB gain:"), dbs, dbs2); + free(dbs); + free(dbs2); + } + } + } else if (control->flags & TYPE_PSWITCH) { + if (!(control->flags & HAS_PSWITCH_1)) { + err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[0], &sw); + if (err >= 0 && !sw) + value_info = casprintf(" [%s]", _("Off")); + } else { + err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[0], &sw); + if (err >= 0) + err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[1], &sw2); + if (err >= 0 && (!sw || !sw2)) + value_info = casprintf(" [%s, %s]", sw ? _("On") : _("Off"), sw2 ? _("On") : _("Off")); + } + } else if (control->flags & TYPE_CSWITCH) { + if (!(control->flags & HAS_CSWITCH_1)) { + err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[0], &sw); + if (err >= 0 && !sw) + value_info = casprintf(" [%s]", _("Off")); + } else { + err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[0], &sw); + if (err >= 0) + err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[1], &sw2); + if (err >= 0 && (!sw || !sw2)) + value_info = casprintf(" [%s, %s]", sw ? _("On") : _("Off"), sw2 ? _("On") : _("Off")); + } + } + item_info = casprintf("%s%s", control->name, value_info ? value_info : ""); + free(value_info); + display_string_in_field(4, info_items_left, item_info, info_items_width, ALIGN_LEFT); + free(item_info); +} + +static void clear_controls_display(void) +{ + int i; + + clickable_clear(5, 0, -1, -1); + wattrset(mixer_widget.window, attrs.mixer_frame); + for (i = 5; i < screen_lines - 1; ++i) + mvwprintw(mixer_widget.window, i, 1, "%*s", screen_cols - 2, ""); +} + +static void center_string(int line, const char *s) +{ + int width = get_mbs_width(s); + if (width <= screen_cols - 2) + mvwaddstr(mixer_widget.window, line, (screen_cols - width) / 2, s); +} + +static void display_unplugged(void) +{ + int lines, top, left; + bool boojum; + + lines = screen_lines - 6; + if (lines < 2) + return; + top = lines / 2; + boojum = lines >= 10 && screen_cols >= 48; + top -= boojum ? 5 : 1; + if (top < 5) + top = 5; + if (boojum) { + left = (screen_cols - 46) / 2; + wattrset(mixer_widget.window, attrs.mixer_text); + mvwaddstr(mixer_widget.window, top + 0, left, "In the midst of the word he was trying to say,"); + mvwaddstr(mixer_widget.window, top + 1, left + 2, "In the midst of his laughter and glee,"); + mvwaddstr(mixer_widget.window, top + 2, left, "He had softly and suddenly vanished away---"); + mvwaddstr(mixer_widget.window, top + 3, left + 2, "For the Snark was a Boojum, you see."); + mvwchgat(mixer_widget.window, top + 3, left + 16, 3, /* ^^^ */ + attrs.mixer_text | A_BOLD, PAIR_NUMBER(attrs.mixer_text), NULL); + mvwaddstr(mixer_widget.window, top + 5, left, "(Lewis Carroll, \"The Hunting of the Snark\")"); + top += 8; + } + wattrset(mixer_widget.window, attrs.errormsg); + center_string(top, _("The sound device was unplugged.")); + center_string(top + 1, _("Press F6 to select another sound card.")); +} + +static void display_no_controls(void) +{ + int y; + const char *msg; + + y = (screen_lines - 6) / 2 - 1; + if (y < 5) + y = 5; + if (y >= screen_lines - 1) + return; + wattrset(mixer_widget.window, attrs.infomsg); + if (view_mode == VIEW_MODE_PLAYBACK && are_there_any_controls()) + msg = _("This sound device does not have any playback controls."); + else if (view_mode == VIEW_MODE_CAPTURE && are_there_any_controls()) + msg = _("This sound device does not have any capture controls."); + else + msg = _("This sound device does not have any controls."); + center_string(y, msg); +} + +static void display_string_centered_in_control(int y, int col, const char *s, int width) +{ + int left, x; + + left = first_control_x + col * (control_width + 1); + x = left + (control_width - width) / 2; + display_string_in_field(y, x, s, width, ALIGN_CENTER); +} + +static void display_control(unsigned int control_index) +{ + struct control *control; + int col; + int i, c; + int left, frame_left; + int bar_height; + double volumes[2]; + int switches[2]; + unsigned int index; + const char *s; + char buf[64]; + int err; + + control = &controls[control_index]; + col = control_index - first_visible_control_index; + left = first_control_x + col * (control_width + 1); + frame_left = left + (control_width - 4) / 2; + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.ctl_frame); + else + wattrset(mixer_widget.window, attrs.ctl_inactive); + if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) { + mvwaddch(mixer_widget.window, base_y - volume_height - 1, frame_left, ACS_ULCORNER); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_URCORNER); + mvwvline(mixer_widget.window, base_y - volume_height, frame_left, ACS_VLINE, volume_height); + mvwvline(mixer_widget.window, base_y - volume_height, frame_left + 3, ACS_VLINE, volume_height); + mvwaddch(mixer_widget.window, base_y, frame_left, + control->flags & TYPE_PSWITCH ? ACS_LTEE : ACS_LLCORNER); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, + control->flags & TYPE_PSWITCH ? ACS_RTEE : ACS_LRCORNER); + } else if (control->flags & TYPE_PSWITCH) { + mvwaddch(mixer_widget.window, base_y, frame_left, ACS_ULCORNER); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_URCORNER); + } + if (control->flags & TYPE_PSWITCH) { + mvwaddch(mixer_widget.window, base_y + 1, frame_left, ACS_VLINE); + mvwaddch(mixer_widget.window, base_y + 1, frame_left + 3, ACS_VLINE); + mvwaddch(mixer_widget.window, base_y + 2, frame_left, ACS_LLCORNER); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_HLINE); + waddch(mixer_widget.window, ACS_LRCORNER); + } + if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) { + double (*get_vol_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t); + + if (control->flags & TYPE_PVOLUME) + get_vol_func = get_normalized_playback_volume; + else + get_vol_func = get_normalized_capture_volume; + volumes[0] = get_vol_func(control->elem, control->volume_channels[0]); + if (control->flags & HAS_VOLUME_1) + volumes[1] = get_vol_func(control->elem, control->volume_channels[1]); + else + volumes[1] = volumes[0]; + + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, 0); + for (c = 0; c < 2; c++) { + bar_height = lrint(volumes[c] * volume_height); + for (i = 0; i < volume_height; ++i) { + chtype ch; + if (i + 1 > bar_height) + ch = ' ' | (control->flags & IS_ACTIVE ? + attrs.ctl_frame : 0); + else { + ch = ACS_CKBOARD; + if (!(control->flags & IS_ACTIVE)) + ; +#ifdef TRICOLOR_VOLUME_BAR + else if (i > volume_height * 8 / 10) + ch |= attrs.ctl_bar_hi; + else if (i > volume_height * 4 / 10) + ch |= attrs.ctl_bar_mi; +#endif + else + ch |= attrs.ctl_bar_lo; + } + mvwaddch(mixer_widget.window, base_y - i - 1, + frame_left + c + 1, ch); + } + } + clickable_set(base_y - volume_height, frame_left + 1, base_y, frame_left + 2, + CMD_MIXER_MOUSE_CLICK_VOLUME_BAR, control_index); + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.mixer_active); + if (!(control->flags & HAS_VOLUME_1)) { + sprintf(buf, "%d", (int)lrint(volumes[0] * 100)); + display_string_in_field(values_y, frame_left - 2, buf, 8, ALIGN_CENTER); + } else { + mvwprintw(mixer_widget.window, values_y, frame_left - 2, + "%3d", (int)lrint(volumes[0] * 100)); + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.ctl_frame); + waddstr(mixer_widget.window, "<>"); + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.mixer_active); + wprintw(mixer_widget.window, "%-3d", (int)lrint(volumes[1] * 100)); + } + } + + if (control->flags & TYPE_PSWITCH) { + err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[0], &switches[0]); + if (err >= 0 && (control->flags & HAS_PSWITCH_1)) + err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[1], &switches[1]); + else + switches[1] = switches[0]; + if (err < 0) + return; + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, 0); + mvwaddch(mixer_widget.window, base_y + 1, frame_left + 1, + switches[0] + /* TRANSLATORS: playback on; one character */ + ? _("O")[0] | (control->flags & IS_ACTIVE ? attrs.ctl_nomute : 0) + /* TRANSLATORS: playback muted; one character */ + : _("M")[0] | (control->flags & IS_ACTIVE ? attrs.ctl_mute : 0)); + waddch(mixer_widget.window, + switches[1] + ? _("O")[0] | (control->flags & IS_ACTIVE ? attrs.ctl_nomute : 0) + : _("M")[0] | (control->flags & IS_ACTIVE ? attrs.ctl_mute : 0)); + clickable_set(base_y + 1, frame_left + 1, base_y + 1, frame_left + 2, + CMD_MIXER_MOUSE_CLICK_MUTE, control_index); + } + + if (control->flags & TYPE_CSWITCH) { + err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[0], &switches[0]); + if (err >= 0 && (control->flags & HAS_CSWITCH_1)) + err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[1], &switches[1]); + else + switches[1] = switches[0]; + if (err < 0) + return; + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, switches[0] ? attrs.ctl_capture : attrs.ctl_nocapture); + /* TRANSLATORS: "left"; no more than two characters */ + display_string_in_field(cswitch_y - 1, frame_left - 2, switches[0] ? _("L") : "", 2, ALIGN_RIGHT); + clickable_set(cswitch_y - 1, frame_left - 2, cswitch_y - 1, frame_left - 1, + CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT), control_index); + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, switches[1] ? attrs.ctl_capture : attrs.ctl_nocapture); + /* TRANSLATORS: "right"; no more than two characters */ + display_string_in_field(cswitch_y - 1, frame_left + 4, switches[1] ? _("R") : "", 2, ALIGN_LEFT); + clickable_set(cswitch_y - 1, frame_left + 4, cswitch_y - 1, frame_left + 5, + CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, RIGHT), control_index); + /* TRANSLATORS: no more than eight characters */ + s = _("CAPTURE"); + if (switches[0] || switches[1]) { + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.ctl_capture); + display_string_in_field(cswitch_y, frame_left - 2, s, 8, ALIGN_CENTER); + } else { + i = get_mbs_width(s); + if (i > 8) + i = 8; + memset(buf, '-', i); + buf[i] = '\0'; + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.ctl_nocapture); + display_string_in_field(cswitch_y, frame_left - 2, buf, 8, ALIGN_CENTER); + } + clickable_set(cswitch_y, frame_left - 2, cswitch_y, frame_left - 2 + 8, + CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT|RIGHT), control_index); + } + + if (control->flags & TYPE_ENUM) { + err = snd_mixer_selem_get_enum_item(control->elem, ffs(control->enum_channel_bits) - 1, &index); + if (err < 0) + return; + err = snd_mixer_selem_get_enum_item_name(control->elem, index, sizeof buf - 1, buf); + if (err < 0) + return; + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.mixer_active); + display_string_centered_in_control(base_y, col, buf, control_width); + clickable_set_relative(mixer_widget.window, 0, -control_name_width, 0, -2, + CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM, control_index); + } + + if ((int)control_index == focus_control_index) { + i = first_control_x + col * (control_width + 1) + (control_width - control_name_width) / 2; + wattrset(mixer_widget.window, attrs.ctl_mark_focus); + mvwaddch(mixer_widget.window, name_y, i - 1, '<'); + mvwaddch(mixer_widget.window, name_y, i + control_name_width, '>'); + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.ctl_label_focus); + else + wattrset(mixer_widget.window, attrs.ctl_label_inactive); + } else { + if (control->flags & IS_ACTIVE) + wattrset(mixer_widget.window, attrs.ctl_label); + else + wattrset(mixer_widget.window, attrs.ctl_label_inactive); + } + display_string_centered_in_control(name_y, col, control->name, control_name_width); + clickable_set_relative(mixer_widget.window, -1, -control_name_width, 0, -2, + CMD_WITH_ARG(CMD_MIXER_FOCUS_CONTROL, control_index), -1); + if (channel_name_y > name_y) { + if (control->flags & IS_MULTICH) { + switch (control->flags & MULTICH_MASK) { + case 0: + default: + s = _("Front"); + break; + case 1: + s = _("Rear"); + break; + case 2: + s = _("Center"); + break; + case 3: + s = _("Woofer"); + break; + case 4: + s = _("Side"); + break; + } + } else { + s = ""; + wattrset(mixer_widget.window, attrs.mixer_frame); + } + display_string_centered_in_control(channel_name_y, col, s, + control_name_width); + } +} + +static void display_scroll_indicators(void) +{ + int y0, y1; + chtype left, right; + + if (screen_too_small) + return; + y0 = screen_lines * 3 / 8; + y1 = screen_lines * 5 / 8; + left = first_visible_control_index > 0 ? ACS_LARROW : ACS_VLINE; + right = first_visible_control_index + visible_controls < (int)controls_count + ? ACS_RARROW : ACS_VLINE; + wattrset(mixer_widget.window, attrs.mixer_frame); + mvwvline(mixer_widget.window, y0, 0, left, y1 - y0 + 1); + mvwvline(mixer_widget.window, y0, screen_cols -1, right, y1 - y0 + 1); + clickable_set(y0, 0, y1, 0, + CMD_WITH_ARG(CMD_MIXER_PREVIOUS, visible_controls), -1); + clickable_set(y0, screen_cols - 1, y1, screen_cols - 1, + CMD_WITH_ARG(CMD_MIXER_NEXT, visible_controls), -1); +} + +void display_controls(void) +{ + int i; + + if (first_visible_control_index > (int)controls_count - visible_controls) + first_visible_control_index = controls_count - visible_controls; + if (first_visible_control_index > focus_control_index) + first_visible_control_index = focus_control_index; + else if (first_visible_control_index < focus_control_index - visible_controls + 1 && visible_controls) + first_visible_control_index = focus_control_index - visible_controls + 1; + + clear_controls_display(); + + display_focus_item_info(); + + if (controls_count > 0) { + if (!screen_too_small) + for (i = 0; i < visible_controls; ++i) + display_control(first_visible_control_index + i); + } else if (unplugged) { + display_unplugged(); + } else if (mixer_device_name) { + display_no_controls(); + } + display_scroll_indicators(); +} + +void compute_controls_layout(void) +{ + bool any_volume, any_pswitch, any_cswitch, any_multich; + int max_width, name_len; + int height, space; + unsigned int i; + + if (controls_count == 0 || screen_too_small) { + visible_controls = 0; + return; + } + + any_volume = FALSE; + any_pswitch = FALSE; + any_cswitch = FALSE; + any_multich = FALSE; + for (i = 0; i < controls_count; ++i) { + if (controls[i].flags & (TYPE_PVOLUME | TYPE_CVOLUME)) + any_volume = 1; + if (controls[i].flags & TYPE_PSWITCH) + any_pswitch = 1; + if (controls[i].flags & TYPE_CSWITCH) + any_cswitch = 1; + if (controls[i].flags & IS_MULTICH) + any_multich = 1; + } + + max_width = 8; + for (i = 0; i < controls_count; ++i) { + name_len = strlen(controls[i].name); + if (name_len > max_width) + max_width = name_len; + } + max_width = (max_width + 1) & ~1; + + control_width = (screen_cols - 3 - (int)controls_count) / controls_count; + if (control_width < 8) + control_width = 8; + if (control_width > max_width) + control_width = max_width; + if (control_width > screen_cols - 4) + control_width = screen_cols - 4; + + visible_controls = (screen_cols - 3) / (control_width + 1); + if (visible_controls > (int)controls_count) + visible_controls = controls_count; + + first_control_x = 2 + (screen_cols - 3 - visible_controls * (control_width + 1)) / 2; + + if (control_width < max_width) + control_name_width = control_width; + else + control_name_width = max_width; + + height = 2; + if (any_volume) + height += 2; + if (any_pswitch) + height += 2; + if (any_cswitch) + height += 1; + if (any_multich) + height += 1; + if (any_volume) { + space = screen_lines - 6 - height; + if (space <= 1) + volume_height = 1; + else if (space <= 10) + volume_height = space; + else + volume_height = 10 + (space - 10) / 2; + height += volume_height; + } + + space = screen_lines - 6 - height; + channel_name_y = screen_lines - 2 - space / 2; + name_y = channel_name_y - any_multich; + values_y = name_y - any_volume; + cswitch_y = values_y - any_cswitch; + base_y = cswitch_y - 1 - 2 * any_pswitch; +} diff --git a/alsamixer/mixer_display.h b/alsamixer/mixer_display.h new file mode 100644 index 0000000..3d65670 --- /dev/null +++ b/alsamixer/mixer_display.h @@ -0,0 +1,10 @@ +#ifndef MIXER_DISPLAY_H_INCLUDED +#define MIXER_DISPLAY_H_INCLUDED + +void init_mixer_layout(void); +void display_card_info(void); +void display_view_mode(void); +void display_controls(void); +void compute_controls_layout(void); + +#endif diff --git a/alsamixer/mixer_widget.c b/alsamixer/mixer_widget.c new file mode 100644 index 0000000..33b9bbc --- /dev/null +++ b/alsamixer/mixer_widget.c @@ -0,0 +1,674 @@ +/* + * mixer_widget.c - mixer widget and keys handling + * Copyright (c) 1998,1999 Tim Janik + * Jaroslav Kysela + * Copyright (c) 2009 Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include "gettext_curses.h" +#include "version.h" +#include "utils.h" +#include "die.h" +#include "mem.h" +#include "colors.h" +#include "widget.h" +#include "textbox.h" +#include "proc_files.h" +#include "card_select.h" +#include "volume_mapping.h" +#include "mixer_clickable.h" +#include "mixer_controls.h" +#include "mixer_display.h" +#include "mixer_widget.h" +#include "bindings.h" + +snd_mixer_t *mixer; +char *mixer_device_name; +bool unplugged; + +struct widget mixer_widget; + +enum view_mode view_mode; + +int focus_control_index; +snd_mixer_selem_id_t *current_selem_id; +unsigned int current_control_flags; + +bool control_values_changed; +bool controls_changed; + +unsigned int mouse_wheel_step = 1; +bool mouse_wheel_focuses_control = 1; + +static int elem_callback(snd_mixer_elem_t *elem ATTRIBUTE_UNUSED, unsigned int mask) +{ + if (mask == SND_CTL_EVENT_MASK_REMOVE) { + controls_changed = TRUE; + } else { + if (mask & SND_CTL_EVENT_MASK_VALUE) + control_values_changed = TRUE; + + if (mask & SND_CTL_EVENT_MASK_INFO) + controls_changed = TRUE; + } + + return 0; +} + +static int mixer_callback(snd_mixer_t *mixer ATTRIBUTE_UNUSED, unsigned int mask, snd_mixer_elem_t *elem) +{ + if (mask & SND_CTL_EVENT_MASK_ADD) { + snd_mixer_elem_set_callback(elem, elem_callback); + controls_changed = TRUE; + } + return 0; +} + +void create_mixer_object(struct snd_mixer_selem_regopt *selem_regopt) +{ + int err; + + err = snd_mixer_open(&mixer, 0); + if (err < 0) + fatal_alsa_error(_("cannot open mixer"), err); + + mixer_device_name = cstrdup(selem_regopt->device); + err = snd_mixer_selem_register(mixer, selem_regopt, NULL); + if (err < 0) + fatal_alsa_error(_("cannot open mixer"), err); + + snd_mixer_set_callback(mixer, mixer_callback); + + err = snd_mixer_load(mixer); + if (err < 0) + fatal_alsa_error(_("cannot load mixer controls"), err); + + err = snd_mixer_selem_id_malloc(¤t_selem_id); + if (err < 0) + fatal_error("out of memory"); +} + +static void set_view_mode(enum view_mode m) +{ + view_mode = m; + create_controls(); +} + +static void close_hctl(void) +{ + free_controls(); + if (mixer_device_name) { + snd_mixer_detach(mixer, mixer_device_name); + free(mixer_device_name); + mixer_device_name = NULL; + } +} + +static void check_unplugged(void) +{ + snd_hctl_t *hctl; + snd_ctl_t *ctl; + unsigned int state; + int err; + + unplugged = FALSE; + if (mixer_device_name) { + err = snd_mixer_get_hctl(mixer, mixer_device_name, &hctl); + if (err >= 0) { + ctl = snd_hctl_ctl(hctl); + /* just any random function that does an ioctl() */ + err = snd_ctl_get_power_state(ctl, &state); + if (err == -ENODEV) + unplugged = TRUE; + } + } +} + +void close_mixer_device(void) +{ + check_unplugged(); + close_hctl(); + + display_card_info(); + set_view_mode(view_mode); +} + +bool select_card_by_name(const char *device_name) +{ + int err; + bool opened; + char *msg; + + close_hctl(); + unplugged = FALSE; + + opened = FALSE; + if (device_name) { + err = snd_mixer_attach(mixer, device_name); + if (err >= 0) + opened = TRUE; + else { + msg = casprintf(_("Cannot open mixer device '%s'."), device_name); + show_alsa_error(msg, err); + free(msg); + } + } + if (opened) { + mixer_device_name = cstrdup(device_name); + + err = snd_mixer_load(mixer); + if (err < 0) + fatal_alsa_error(_("cannot load mixer controls"), err); + } + + display_card_info(); + set_view_mode(view_mode); + return opened; +} + +static void show_help(void) +{ + const char *help[] = { + _("Esc Exit"), + _("F1 ? H Help"), + _("F2 / System information"), + _("F3 Show playback controls"), + _("F4 Show capture controls"), + _("F5 Show all controls"), + _("Tab Toggle view mode (F3/F4/F5)"), + _("F6 S Select sound card"), + _("L Redraw screen"), + "", + _("Left Move to the previous control"), + _("Right Move to the next control"), + "", + _("Up/Down Change volume"), + _("+ - Change volume"), + _("Page Up/Dn Change volume in big steps"), + _("End Set volume to 0%"), + _("0-9 Set volume to 0%-90%"), + _("Q W E Increase left/both/right volumes"), + /* TRANSLATORS: or Y instead of Z */ + _("Z X C Decrease left/both/right volumes"), + _("B Balance left and right volumes"), + "", + _("M Toggle mute"), + /* TRANSLATORS: or , . */ + _("< > Toggle left/right mute"), + "", + _("Space Toggle capture"), + /* TRANSLATORS: or Insert Delete */ + _("; ' Toggle left/right capture"), + "", + _("Authors:"), + _(" Tim Janik"), + _(" Jaroslav Kysela "), + _(" Clemens Ladisch "), + }; + show_text(help, ARRAY_SIZE(help), _("Help")); +} + +void refocus_control(void) +{ + if (focus_control_index >= 0 && + focus_control_index < (int)controls_count) { + snd_mixer_selem_get_id(controls[focus_control_index].elem, current_selem_id); + current_control_flags = controls[focus_control_index].flags; + } + + display_controls(); +} + +static struct control *get_focus_control(unsigned int type) +{ + if (focus_control_index >= 0 && + focus_control_index < (int)controls_count && + (controls[focus_control_index].flags & IS_ACTIVE) && + (controls[focus_control_index].flags & type)) + return &controls[focus_control_index]; + else + return NULL; +} + +static void change_enum_to_percent(struct control *control, int value) +{ + unsigned int i; + unsigned int index; + unsigned int new_index; + int items; + int err; + + i = ffs(control->enum_channel_bits) - 1; + err = snd_mixer_selem_get_enum_item(control->elem, i, &index); + if (err < 0) + return; + new_index = index; + if (value == 0) { + new_index = 0; + } else if (value == 100) { + items = snd_mixer_selem_get_enum_items(control->elem); + if (items < 1) + return; + new_index = items - 1; + } + if (new_index == index) + return; + for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i) + if (control->enum_channel_bits & (1 << i)) + snd_mixer_selem_set_enum_item(control->elem, i, new_index); +} + +static void change_enum_relative(struct control *control, int delta) +{ + int items; + unsigned int i; + unsigned int index; + int new_index; + int err; + + items = snd_mixer_selem_get_enum_items(control->elem); + if (items < 1) + return; + err = snd_mixer_selem_get_enum_item(control->elem, 0, &index); + if (err < 0) + return; + new_index = (int)index + delta; + if (new_index < 0) + new_index = 0; + else if (new_index >= items) + new_index = items - 1; + if (new_index == (int)index) + return; + for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i) + if (control->enum_channel_bits & (1 << i)) + snd_mixer_selem_set_enum_item(control->elem, i, new_index); +} + +static void change_volume_to_percent(struct control *control, int value, unsigned int channels) +{ + int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int); + + if (!(control->flags & HAS_VOLUME_1)) + channels = LEFT; + if (control->flags & TYPE_PVOLUME) + set_func = set_normalized_playback_volume; + else + set_func = set_normalized_capture_volume; + if (channels & LEFT) + set_func(control->elem, control->volume_channels[0], value / 100.0, 0); + if (channels & RIGHT) + set_func(control->elem, control->volume_channels[1], value / 100.0, 0); +} + +static double clamp_volume(double v) +{ + if (v < 0) + return 0; + if (v > 1) + return 1; + return v; +} + +static void change_volume_relative(struct control *control, int delta, unsigned int channels) +{ + double (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t); + int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int); + double left = 0, right = 0; + int dir; + + if (!(control->flags & HAS_VOLUME_1)) + channels = LEFT; + if (control->flags & TYPE_PVOLUME) { + get_func = get_normalized_playback_volume; + set_func = set_normalized_playback_volume; + } else { + get_func = get_normalized_capture_volume; + set_func = set_normalized_capture_volume; + } + if (channels & LEFT) + left = get_func(control->elem, control->volume_channels[0]); + if (channels & RIGHT) + right = get_func(control->elem, control->volume_channels[1]); + dir = delta > 0 ? 1 : -1; + if (channels & LEFT) { + left = clamp_volume(left + delta / 100.0); + set_func(control->elem, control->volume_channels[0], left, dir); + } + if (channels & RIGHT) { + right = clamp_volume(right + delta / 100.0); + set_func(control->elem, control->volume_channels[1], right, dir); + } +} + +static void change_control_to_percent(int value, unsigned int channels) +{ + struct control *control; + + control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME | TYPE_ENUM); + if (!control) + return; + if (control->flags & TYPE_ENUM) + change_enum_to_percent(control, value); + else + change_volume_to_percent(control, value, channels); + display_controls(); +} + +static void change_control_relative(int delta, unsigned int channels) +{ + struct control *control; + + control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME | TYPE_ENUM); + if (!control) + return; + if (control->flags & TYPE_ENUM) + change_enum_relative(control, delta); + else + change_volume_relative(control, delta, channels); + display_controls(); +} + +static void toggle_switches(unsigned int type, unsigned int channels) +{ + struct control *control; + unsigned int switch_1_mask; + int (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int *); + int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int); + snd_mixer_selem_channel_id_t channel_ids[2]; + int left, right; + int err; + + control = get_focus_control(type); + if (!control) + return; + if (type == TYPE_PSWITCH) { + switch_1_mask = HAS_PSWITCH_1; + get_func = snd_mixer_selem_get_playback_switch; + set_func = snd_mixer_selem_set_playback_switch; + channel_ids[0] = control->pswitch_channels[0]; + channel_ids[1] = control->pswitch_channels[1]; + } else { + switch_1_mask = HAS_CSWITCH_1; + get_func = snd_mixer_selem_get_capture_switch; + set_func = snd_mixer_selem_set_capture_switch; + channel_ids[0] = control->cswitch_channels[0]; + channel_ids[1] = control->cswitch_channels[1]; + } + if (!(control->flags & switch_1_mask)) + channels = LEFT; + if (channels & LEFT) { + err = get_func(control->elem, channel_ids[0], &left); + if (err < 0) + return; + } + if (channels & RIGHT) { + err = get_func(control->elem, channel_ids[1], &right); + if (err < 0) + return; + } + if (channels & LEFT) + set_func(control->elem, channel_ids[0], !left); + if (channels & RIGHT) + set_func(control->elem, channel_ids[1], !right); + display_controls(); +} + +static void toggle_mute(unsigned int channels) +{ + toggle_switches(TYPE_PSWITCH, channels); +} + +static void toggle_capture(unsigned int channels) +{ + toggle_switches(TYPE_CSWITCH, channels); +} + +static void balance_volumes(void) +{ + struct control *control; + double left, right; + + control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME); + if (!control || !(control->flags & HAS_VOLUME_1)) + return; + if (control->flags & TYPE_PVOLUME) { + left = get_normalized_playback_volume(control->elem, control->volume_channels[0]); + right = get_normalized_playback_volume(control->elem, control->volume_channels[1]); + } else { + left = get_normalized_capture_volume(control->elem, control->volume_channels[0]); + right = get_normalized_capture_volume(control->elem, control->volume_channels[1]); + } + left = (left + right) / 2; + if (control->flags & TYPE_PVOLUME) { + set_normalized_playback_volume(control->elem, control->volume_channels[0], left, 0); + set_normalized_playback_volume(control->elem, control->volume_channels[1], left, 0); + } else { + set_normalized_capture_volume(control->elem, control->volume_channels[0], left, 0); + set_normalized_capture_volume(control->elem, control->volume_channels[1], left, 0); + } + display_controls(); +} + +static int on_mouse_key() { + MEVENT m; + command_enum cmd = 0; + unsigned int channels = LEFT | RIGHT; + unsigned int index; + struct control *control; + struct clickable_rect *rect; + + if (getmouse(&m) == ERR) + return 0; + + if (m.bstate & ( + BUTTON1_PRESSED|BUTTON1_RELEASED| + BUTTON2_PRESSED|BUTTON2_RELEASED| + BUTTON3_PRESSED|BUTTON3_RELEASED)) + return 0; + + rect = clickable_find(m.y, m.x); + if (rect) + cmd = rect->command; + +#if NCURSES_MOUSE_VERSION > 1 + if (m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED|BUTTON5_CLICKED|BUTTON5_PRESSED)) { + switch (cmd) { + case CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM: + focus_control_index = rect->arg1; + return CMD_WITH_ARG(( + m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED) + ? CMD_MIXER_CONTROL_UP + : CMD_MIXER_CONTROL_DOWN + ), 1); + + case CMD_MIXER_MOUSE_CLICK_VOLUME_BAR: + if (mouse_wheel_focuses_control) + focus_control_index = rect->arg1; + /* fall through */ + + default: + return CMD_WITH_ARG(( + m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED) + ? CMD_MIXER_CONTROL_UP + : CMD_MIXER_CONTROL_DOWN + ), mouse_wheel_step); + } + } +#endif + + /* If the rectangle has got an argument (value != -1) it is used for + * setting `focus_control_index` */ + if (rect && rect->arg1 >= 0) + focus_control_index = rect->arg1; + + switch (cmd) { + case CMD_MIXER_MOUSE_CLICK_VOLUME_BAR: + if (m.bstate & (BUTTON3_CLICKED|BUTTON3_DOUBLE_CLICKED|BUTTON3_TRIPLE_CLICKED)) + channels = m.x - rect->x1 + 1; + return CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT_LEFT + channels - 1, + (100 * (rect->y2 - m.y) / (rect->y2 - rect->y1)) // volume + ); + + case CMD_MIXER_MOUSE_CLICK_MUTE: + if (m.bstate & (BUTTON3_CLICKED|BUTTON3_DOUBLE_CLICKED|BUTTON3_TRIPLE_CLICKED)) + channels = m.x - rect->x1 + 1; + return CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, channels); + + case CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM: + control = get_focus_control(TYPE_ENUM); + if (control && + (snd_mixer_selem_get_enum_item(control->elem, 0, &index) >= 0)) { + return (index == 0 + ? CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 100) + : CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1)); + } + break; + + default: + return cmd; // non-mouse command + } + + return 0; // failed mouse command +} + +static void on_handle_key(int key) +{ + int arg; + command_enum cmd; + + if (key == KEY_MOUSE) + cmd = on_mouse_key(); + else if (key < (int)ARRAY_SIZE(mixer_bindings)) + cmd = mixer_bindings[key]; + else + return; + + arg = CMD_GET_ARGUMENT(cmd); + cmd = CMD_GET_COMMAND(cmd); + + switch (cmd) { + case CMD_MIXER_CONTROL_DOWN_LEFT: + case CMD_MIXER_CONTROL_DOWN_RIGHT: + case CMD_MIXER_CONTROL_DOWN: + arg = (-arg); + /* fall through */ + case CMD_MIXER_CONTROL_UP_LEFT: + case CMD_MIXER_CONTROL_UP_RIGHT: + case CMD_MIXER_CONTROL_UP: + change_control_relative(arg, cmd % 4); + break; + case CMD_MIXER_CONTROL_SET_PERCENT_LEFT: + case CMD_MIXER_CONTROL_SET_PERCENT_RIGHT: + case CMD_MIXER_CONTROL_SET_PERCENT: + change_control_to_percent(arg, cmd % 4); + break; + case CMD_MIXER_CLOSE: + mixer_widget.close(); + break; + case CMD_MIXER_HELP: + show_help(); + break; + case CMD_MIXER_SYSTEM_INFORMATION: + create_proc_files_list(); + break; + case CMD_MIXER_TOGGLE_VIEW_MODE: + arg = (view_mode + 1) % VIEW_MODE_COUNT; + /* fall through */ + case CMD_MIXER_SET_VIEW_MODE: + set_view_mode((enum view_mode)(arg)); + break; + case CMD_MIXER_SELECT_CARD: + create_card_select_list(); + break; + case CMD_MIXER_REFRESH: + clearok(mixer_widget.window, TRUE); + display_controls(); + break; + case CMD_MIXER_PREVIOUS: + arg = (-arg); + /* fall through */ + case CMD_MIXER_NEXT: + arg = focus_control_index + arg; + /* fall through */ + case CMD_MIXER_FOCUS_CONTROL: + focus_control_index = arg; + if (focus_control_index < 0) + focus_control_index = 0; + else if (focus_control_index >= (int)controls_count) + focus_control_index = controls_count - 1; + refocus_control(); + break; + case CMD_MIXER_TOGGLE_MUTE: + toggle_mute(arg); + break; + case CMD_MIXER_TOGGLE_CAPTURE: + toggle_capture(arg); + break; + case CMD_MIXER_BALANCE_CONTROL: + balance_volumes(); + break; + } +} + +static void create(void) +{ + static const char title[] = " AlsaMixer v" SND_UTIL_VERSION_STR " "; + + widget_init(&mixer_widget, screen_lines, screen_cols, 0, 0, + attrs.mixer_frame, WIDGET_BORDER); + if (screen_cols >= (int)(sizeof(title) - 1) + 2) { + wattrset(mixer_widget.window, attrs.mixer_active); + mvwaddstr(mixer_widget.window, 0, (screen_cols - (sizeof(title) - 1)) / 2, title); + } + init_mixer_layout(); + display_card_info(); + set_view_mode(view_mode); +} + +static void on_window_size_changed(void) +{ + create(); +} + +static void on_close(void) +{ + widget_free(&mixer_widget); +} + +void mixer_shutdown(void) +{ + free_controls(); + if (mixer) + snd_mixer_close(mixer); + if (current_selem_id) + snd_mixer_selem_id_free(current_selem_id); +} + +struct widget mixer_widget = { + .handle_key = on_handle_key, + .window_size_changed = on_window_size_changed, + .close = on_close, +}; + +void create_mixer_widget(void) +{ + create(); +} diff --git a/alsamixer/mixer_widget.h b/alsamixer/mixer_widget.h new file mode 100644 index 0000000..3b6f474 --- /dev/null +++ b/alsamixer/mixer_widget.h @@ -0,0 +1,45 @@ +#ifndef MIXER_WIDGET_H_INCLUDED +#define MIXER_WIDGET_H_INCLUDED + +#include CURSESINC +#include +#include "widget.h" + +enum view_mode { + VIEW_MODE_PLAYBACK, + VIEW_MODE_CAPTURE, + VIEW_MODE_ALL, + VIEW_MODE_COUNT, +}; + +enum channel_mask { + LEFT = 1, + RIGHT = 2, +}; + +extern snd_mixer_t *mixer; +extern char *mixer_device_name; +extern bool unplugged; + +extern struct widget mixer_widget; + +extern enum view_mode view_mode; + +extern int focus_control_index; +extern snd_mixer_selem_id_t *current_selem_id; +extern unsigned int current_control_flags; + +extern bool control_values_changed; +extern bool controls_changed; + +extern unsigned int mouse_wheel_step; +extern bool mouse_wheel_focuses_control; + +void create_mixer_object(struct snd_mixer_selem_regopt *selem_regopt); +void create_mixer_widget(void); +void mixer_shutdown(void); +void close_mixer_device(void); +bool select_card_by_name(const char *device_name); +void refocus_control(void); + +#endif diff --git a/alsamixer/proc_files.c b/alsamixer/proc_files.c new file mode 100644 index 0000000..0ca003b --- /dev/null +++ b/alsamixer/proc_files.c @@ -0,0 +1,107 @@ +/* + * proc_files.c - shows ALSA system information files + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include "gettext_curses.h" +#include "utils.h" +#include "die.h" +#include "mem.h" +#include "colors.h" +#include "widget.h" +#include "textbox.h" +#include "proc_files.h" +#include "menu_widget.h" + +static struct widget proc_widget; +static ITEM *items[7]; +static unsigned int items_count; +static MENU *menu; + +static void on_handle_key(int key) +{ + ITEM *item; + + switch (menu_widget_handle_key(menu, key)) { + case KEY_ENTER: + item = current_item(menu); + if (item) + show_textfile(item_name(item)); + break; + case KEY_CANCEL: + proc_widget.close(); + break; + } +} + +static void create(void) +{ + menu_widget_create(&proc_widget, menu, _("Select File")); +} + +static void on_close(void) +{ + unsigned int i; + + unpost_menu(menu); + free_menu(menu); + for (i = 0; i < items_count; ++i) + free_item(items[i]); + widget_free(&proc_widget); +} + +static void add_item(const char *file_name) +{ + if (access(file_name, F_OK) == 0) { + items[items_count] = new_item(file_name, NULL); + if (!items[items_count]) + fatal_error("cannot create menu item"); + ++items_count; + assert(items_count < ARRAY_SIZE(items)); + } +} + +static struct widget proc_widget = { + .handle_key = on_handle_key, + .window_size_changed = create, + .close = on_close, +}; + +void create_proc_files_list(void) +{ + items_count = 0; + add_item("/proc/asound/version"); + add_item("/proc/asound/cards"); + add_item("/proc/asound/devices"); + add_item("/proc/asound/oss/devices"); + add_item("/proc/asound/timers"); + add_item("/proc/asound/pcm"); + items[items_count] = NULL; + + menu = new_menu(items); + if (!menu) + fatal_error("cannot create menu"); + set_menu_fore(menu, attrs.menu_selected); + set_menu_back(menu, attrs.menu); + set_menu_mark(menu, NULL); + menu_opts_off(menu, O_SHOWDESC); + + create(); +} diff --git a/alsamixer/proc_files.h b/alsamixer/proc_files.h new file mode 100644 index 0000000..8862c71 --- /dev/null +++ b/alsamixer/proc_files.h @@ -0,0 +1,6 @@ +#ifndef PROC_FILES_H_INCLUDED +#define PROC_FILES_H_INCLUDED + +void create_proc_files_list(void); + +#endif diff --git a/alsamixer/textbox.c b/alsamixer/textbox.c new file mode 100644 index 0000000..930ce73 --- /dev/null +++ b/alsamixer/textbox.c @@ -0,0 +1,335 @@ +/* + * textbox.c - show a text box for messages, files or help + * Copyright (c) 1998,1999 Tim Janik + * Jaroslav Kysela + * Copyright (c) 2009 Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include CURSESINC +#include +#include "gettext_curses.h" +#include "utils.h" +#include "die.h" +#include "mem.h" +#include "colors.h" +#include "widget.h" +#include "textbox.h" +#include "bindings.h" + +static void create_text_box(const char *const *lines, unsigned int count, + const char *title, int attrs); + +void show_error(const char *msg, int err) +{ + const char *lines[2]; + unsigned int count; + + lines[0] = msg; + count = 1; + if (err) { + lines[1] = strerror(err); + count = 2; + } + create_text_box(lines, count, _("Error"), attrs.errormsg); +} + +void show_alsa_error(const char *msg, int err) +{ + const char *lines[2]; + unsigned int count; + + lines[0] = msg; + count = 1; + if (err < 0) { + lines[1] = snd_strerror(err); + count = 2; + } + create_text_box(lines, count, _("Error"), attrs.errormsg); +} + +void show_textfile(const char *file_name) +{ + char *buf; + unsigned int file_size; + unsigned int line_count; + unsigned int i; + const char **lines; + const char *start_line; + + buf = read_file(file_name, &file_size); + if (!buf) { + int err = errno; + buf = casprintf(_("Cannot open file \"%s\"."), file_name); + show_error(buf, err); + return; + } + line_count = 0; + for (i = 0; i < file_size; ++i) + line_count += buf[i] == '\n'; + lines = ccalloc(line_count, sizeof *lines); + line_count = 0; + start_line = buf; + for (i = 0; i < file_size; ++i) { + if (buf[i] == '\n') { + lines[line_count++] = start_line; + buf[i] = '\0'; + start_line = &buf[i + 1]; + } + if (buf[i] == '\t') + buf[i] = ' '; + } + create_text_box(lines, line_count, file_name, attrs.textbox); + free(lines); + free(buf); +} + +void show_text(const char *const *lines, unsigned int count, const char *title) +{ + create_text_box(lines, count, title, attrs.textbox); +} + +/**********************************************************************/ + +static struct widget text_widget; +static char *title; +static int widget_attrs; +static char **text_lines; +static unsigned int text_lines_count; +static int max_line_width; +static int text_box_y; +static int text_box_x; +static int max_scroll_y; +static int max_scroll_x; +static int current_top; +static int current_left; + +static void update_text_lines(void) +{ + int i; + int width; + const char *line_begin; + const char *line_end; + int cur_y, cur_x; + int rest_of_line; + + for (i = 0; i < text_box_y; ++i) { + width = current_left; + line_begin = mbs_at_width(text_lines[current_top + i], &width, 1); + wmove(text_widget.window, i + 1, 1); + if (width > current_left) + waddch(text_widget.window, ' '); + if (*line_begin != '\0') { + width = text_box_x - (width > current_left); + line_end = mbs_at_width(line_begin, &width, -1); + if (width) + waddnstr(text_widget.window, line_begin, + line_end - line_begin); + } + getyx(text_widget.window, cur_y, cur_x); + if (cur_y == i + 1) { + rest_of_line = text_box_x + 1 - cur_x; + if (rest_of_line > 0) + wprintw(text_widget.window, "%*s", rest_of_line, ""); + } + } +} + +static void update_y_scroll_bar(void) +{ + int length; + int begin; + + if (max_scroll_y <= 0 || text_lines_count == 0) + return; + length = text_box_y * text_box_y / text_lines_count; + if (length >= text_box_y) + return; + begin = current_top * (text_box_y - length) / max_scroll_y; + mvwvline(text_widget.window, 1, text_box_x + 1, ' ', text_box_y); + mvwvline(text_widget.window, begin + 1, text_box_x + 1, ACS_BOARD, length); +} + +static void update_x_scroll_bar(void) +{ + int length; + int begin; + + if (max_scroll_x <= 0 || max_line_width <= 0) + return; + length = text_box_x * text_box_x / max_line_width; + if (length >= text_box_x) + return; + begin = current_left * (text_box_x - length) / max_scroll_x; + mvwhline(text_widget.window, text_box_y + 1, 1, ' ', text_box_x); + mvwhline(text_widget.window, text_box_y + 1, begin + 1, ACS_BOARD, length); +} + +static void move_x(int delta) +{ + int left; + + left = current_left + delta; + if (left < 0) + left = 0; + else if (left > max_scroll_x) + left = max_scroll_x; + if (left != current_left) { + current_left = left; + update_text_lines(); + update_x_scroll_bar(); + } +} + +static void move_y(int delta) +{ + int top; + + top = current_top + delta; + if (top < 0) + top = 0; + else if (top > max_scroll_y) + top = max_scroll_y; + if (top != current_top) { + current_top = top; + update_text_lines(); + update_y_scroll_bar(); + } +} + +static void on_handle_key(int key) +{ + if (key >= (int)ARRAY_SIZE(textbox_bindings)) + return; + + switch (textbox_bindings[key]) { + case CMD_TEXTBOX_CLOSE: + text_widget.close(); + break; + case CMD_TEXTBOX_DOWN: + move_y(1); + break; + case CMD_TEXTBOX_UP: + move_y(-1); + break; + case CMD_TEXTBOX_LEFT: + move_x(-1); + break; + case CMD_TEXTBOX_RIGHT: + move_x(1); + break; + case CMD_TEXTBOX_PAGE_DOWN: + move_y(text_box_y); + break; + case CMD_TEXTBOX_PAGE_UP: + move_y(-text_box_y); + break; + case CMD_TEXTBOX_TOP: + move_x(-max_scroll_x); + break; + case CMD_TEXTBOX_BOTTOM: + move_x(max_scroll_x); + break; + case CMD_TEXTBOX_PAGE_RIGHT: + move_x(8); + break; + case CMD_TEXTBOX_PAGE_LEFT: + move_x(-8); + break; + } +} + +static bool create(void) +{ + int len, width; + + if (screen_lines < 3 || screen_cols < 8) { + text_widget.close(); + beep(); + return FALSE; + } + + width = max_line_width; + len = get_mbs_width(title) + 2; + if (width < len) + width = len; + + text_box_y = text_lines_count; + if (text_box_y > screen_lines - 2) + text_box_y = screen_lines - 2; + max_scroll_y = text_lines_count - text_box_y; + text_box_x = width; + if (text_box_x > screen_cols - 2) + text_box_x = screen_cols - 2; + max_scroll_x = max_line_width - text_box_x; + + widget_init(&text_widget, text_box_y + 2, text_box_x + 2, + SCREEN_CENTER, SCREEN_CENTER, widget_attrs, WIDGET_BORDER); + mvwprintw(text_widget.window, 0, (text_box_x + 2 - get_mbs_width(title) - 2) / 2, " %s ", title); + + if (current_top > max_scroll_y) + current_top = max_scroll_y; + if (current_left > max_scroll_x) + current_left = max_scroll_x; + update_text_lines(); + update_y_scroll_bar(); + update_x_scroll_bar(); + return TRUE; +} + +static void on_window_size_changed(void) +{ + create(); +} + +static void on_close(void) +{ + unsigned int i; + + for (i = 0; i < text_lines_count; ++i) + free(text_lines[i]); + free(text_lines); + widget_free(&text_widget); +} + +static struct widget text_widget = { + .handle_key = on_handle_key, + .window_size_changed = on_window_size_changed, + .close = on_close, +}; + +static void create_text_box(const char *const *lines, unsigned int count, + const char *title_, int attrs) +{ + unsigned int i; + + text_lines = ccalloc(count, sizeof *text_lines); + for (i = 0; i < count; ++i) + text_lines[i] = cstrdup(lines[i]); + text_lines_count = count; + max_line_width = get_max_mbs_width(lines, count); + title = cstrdup(title_); + widget_attrs = attrs; + + current_top = 0; + current_left = 0; + + create(); +} diff --git a/alsamixer/textbox.h b/alsamixer/textbox.h new file mode 100644 index 0000000..7dc290b --- /dev/null +++ b/alsamixer/textbox.h @@ -0,0 +1,10 @@ +#ifndef TEXTBOX_H_INCLUDED +#define TEXTBOX_H_INCLUDED + +void show_error(const char *msg, int err); +void show_alsa_error(const char *msg, int err); +void show_text(const char *const *text_lines, unsigned int count, + const char *title); +void show_textfile(const char *file_name); + +#endif diff --git a/alsamixer/utils.c b/alsamixer/utils.c new file mode 100644 index 0000000..64c69f2 --- /dev/null +++ b/alsamixer/utils.c @@ -0,0 +1,148 @@ +/* + * utils.c - multibyte-string helpers + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE +#endif +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include "utils.h" +#include "mem.h" + +/* + * mbs_at_width - compute screen position in a string + * + * For displaying strings on the screen, we have to know how many character + * cells are occupied. This function calculates the position in a multibyte + * string that is at a desired position. + * + * Parameters: + * s: the string + * width: on input, the desired number of character cells; on output, the actual + * position, in character cells, of the return value + * dir: -1 or 1; in which direction to round if a multi-column character goes + * over the desired width + * + * Return value: + * Pointer to the place in the string that is as near the desired width as + * possible. If the string is too short, the return value points to the + * terminating zero. If the last character is a multi-column character that + * goes over the desired width, the return value may be one character cell + * earlier or later than desired, depending on the dir parameter. + * In any case, the return value points after any zero-width characters that + * follow the last character. + */ +const char *mbs_at_width(const char *s, int *width, int dir) +{ + size_t len; + wchar_t wc; + int bytes; + int width_so_far, w; + + if (*width <= 0) + return s; + mbtowc(NULL, NULL, 0); /* reset shift state */ + len = strlen(s); + width_so_far = 0; + while (len && (bytes = mbtowc(&wc, s, len)) > 0) { + w = wcwidth(wc); + if (width_so_far + w > *width && dir < 0) + break; + if (w >= 0) + width_so_far += w; + s += bytes; + len -= bytes; + if (width_so_far >= *width) { + while (len && (bytes = mbtowc(&wc, s, len)) > 0) { + w = wcwidth(wc); + if (w != 0) + break; + s += bytes; + len -= bytes; + } + break; + } + } + *width = width_so_far; + return s; +} + +/* + * get_mbs_width - compute screen width of a string + */ +unsigned int get_mbs_width(const char *s) +{ + int width; + + width = INT_MAX; + mbs_at_width(s, &width, 1); + return width; +} + +/* + * get_max_mbs_width - get width of longest string in an array + */ +unsigned int get_max_mbs_width(const char *const *s, unsigned int count) +{ + unsigned int max_width, i, len; + + max_width = 0; + for (i = 0; i < count; ++i) { + len = get_mbs_width(s[i]); + if (len > max_width) + max_width = len; + } + return max_width; +} + +#define MAX_FILE_SIZE 1048576 +char *read_file(const char *file_name, unsigned int *file_size) +{ + FILE *f; + int err; + char *buf; + unsigned int allocated = 2048; + unsigned int bytes_read; + + f = fopen(file_name, "r"); + if (!f) { + err = errno; + errno = err; + return NULL; + } + *file_size = 0; + buf = NULL; + do { + allocated *= 2; + buf = crealloc(buf, allocated); + bytes_read = fread(buf + *file_size, 1, allocated - *file_size, f); + *file_size += bytes_read; + } while (*file_size == allocated && allocated < MAX_FILE_SIZE); + fclose(f); + if (*file_size > 0 && buf[*file_size - 1] != '\n' && *file_size < allocated) { + buf[*file_size] = '\n'; + ++*file_size; + } + return buf; +} + diff --git a/alsamixer/utils.h b/alsamixer/utils.h new file mode 100644 index 0000000..8301311 --- /dev/null +++ b/alsamixer/utils.h @@ -0,0 +1,11 @@ +#ifndef UTILS_H_INCLUDED +#define UTILS_H_INCLUDED + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof *(a)) + +unsigned int get_mbs_width(const char *s); +unsigned int get_max_mbs_width(const char *const *s, unsigned int count); +const char *mbs_at_width(const char *s, int *width, int dir); +char *read_file(const char *file_name, unsigned int *file_size); + +#endif diff --git a/alsamixer/volume_mapping.c b/alsamixer/volume_mapping.c new file mode 100644 index 0000000..f34801c --- /dev/null +++ b/alsamixer/volume_mapping.c @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2010 Clemens Ladisch + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * The functions in this file map the value ranges of ALSA mixer controls onto + * the interval 0..1. + * + * The mapping is designed so that the position in the interval is proportional + * to the volume as a human ear would perceive it (i.e., the position is the + * cubic root of the linear sample multiplication factor). For controls with + * a small range (24 dB or less), the mapping is linear in the dB values so + * that each step has the same size visually. Only for controls without dB + * information, a linear mapping of the hardware volume register values is used + * (this is the same algorithm as used in the old alsamixer). + * + * When setting the volume, 'dir' is the rounding direction: + * -1/0/1 = down/nearest/up. + */ + +#define _ISOC99_SOURCE /* lrint() */ +#include "aconfig.h" +#include +#include +#include "volume_mapping.h" + +#define MAX_LINEAR_DB_SCALE 24 + +static inline bool use_linear_dB_scale(long dBmin, long dBmax) +{ + return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100; +} + +static long lrint_dir(double x, int dir) +{ + if (dir > 0) + return lrint(ceil(x)); + else if (dir < 0) + return lrint(floor(x)); + else + return lrint(x); +} + +enum ctl_dir { PLAYBACK, CAPTURE }; + +static int (* const get_dB_range[2])(snd_mixer_elem_t *, long *, long *) = { + snd_mixer_selem_get_playback_dB_range, + snd_mixer_selem_get_capture_dB_range, +}; +static int (* const get_raw_range[2])(snd_mixer_elem_t *, long *, long *) = { + snd_mixer_selem_get_playback_volume_range, + snd_mixer_selem_get_capture_volume_range, +}; +static int (* const get_dB[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = { + snd_mixer_selem_get_playback_dB, + snd_mixer_selem_get_capture_dB, +}; +static int (* const get_raw[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = { + snd_mixer_selem_get_playback_volume, + snd_mixer_selem_get_capture_volume, +}; +static int (* const set_dB[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long, int) = { + snd_mixer_selem_set_playback_dB, + snd_mixer_selem_set_capture_dB, +}; +static int (* const set_raw[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long) = { + snd_mixer_selem_set_playback_volume, + snd_mixer_selem_set_capture_volume, +}; + +static double get_normalized_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + enum ctl_dir ctl_dir) +{ + long min, max, value; + double normalized, min_norm; + int err; + + err = get_dB_range[ctl_dir](elem, &min, &max); + if (err < 0 || min >= max) { + err = get_raw_range[ctl_dir](elem, &min, &max); + if (err < 0 || min == max) + return 0; + + err = get_raw[ctl_dir](elem, channel, &value); + if (err < 0) + return 0; + + return (value - min) / (double)(max - min); + } + + err = get_dB[ctl_dir](elem, channel, &value); + if (err < 0) + return 0; + + if (use_linear_dB_scale(min, max)) + return (value - min) / (double)(max - min); + + normalized = pow(10, (value - max) / 6000.0); + if (min != SND_CTL_TLV_DB_GAIN_MUTE) { + min_norm = pow(10, (min - max) / 6000.0); + normalized = (normalized - min_norm) / (1 - min_norm); + } + + return normalized; +} + +static int set_normalized_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir, + enum ctl_dir ctl_dir) +{ + long min, max, value; + double min_norm; + int err; + + err = get_dB_range[ctl_dir](elem, &min, &max); + if (err < 0 || min >= max) { + err = get_raw_range[ctl_dir](elem, &min, &max); + if (err < 0) + return err; + + value = lrint_dir(volume * (max - min), dir) + min; + return set_raw[ctl_dir](elem, channel, value); + } + + if (use_linear_dB_scale(min, max)) { + value = lrint_dir(volume * (max - min), dir) + min; + return set_dB[ctl_dir](elem, channel, value, dir); + } + + if (min != SND_CTL_TLV_DB_GAIN_MUTE) { + min_norm = pow(10, (min - max) / 6000.0); + volume = volume * (1 - min_norm) + min_norm; + } + if (volume <= 0) + volume = 1e-36; + value = lrint_dir(6000.0 * log10(volume), dir) + max; + return set_dB[ctl_dir](elem, channel, value, dir); +} + +double get_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel) +{ + return get_normalized_volume(elem, channel, PLAYBACK); +} + +double get_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel) +{ + return get_normalized_volume(elem, channel, CAPTURE); +} + +int set_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir) +{ + return set_normalized_volume(elem, channel, volume, dir, PLAYBACK); +} + +int set_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir) +{ + return set_normalized_volume(elem, channel, volume, dir, CAPTURE); +} diff --git a/alsamixer/volume_mapping.h b/alsamixer/volume_mapping.h new file mode 100644 index 0000000..d4251d6 --- /dev/null +++ b/alsamixer/volume_mapping.h @@ -0,0 +1,19 @@ +#ifndef VOLUME_MAPPING_H_INCLUDED +#define VOLUME_MAPPING_H_INCLUDED + +#include + +double get_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel); +double get_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel); +int set_normalized_playback_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir); +int set_normalized_capture_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t channel, + double volume, + int dir); + +#endif diff --git a/alsamixer/widget.c b/alsamixer/widget.c new file mode 100644 index 0000000..5fedaba --- /dev/null +++ b/alsamixer/widget.c @@ -0,0 +1,139 @@ +/* + * widget.c - handles widget objects and the widget stack + * Copyright (c) Clemens Ladisch + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include "die.h" +#include "widget.h" + +int screen_lines; +int screen_cols; + +static int cursor_visibility = -1; + +static void update_cursor_visibility(void) +{ + const struct widget *active_widget; + + active_widget = get_active_widget(); + if (active_widget && + active_widget->cursor_visibility != cursor_visibility) { + cursor_visibility = active_widget->cursor_visibility; + curs_set(cursor_visibility); + } +} + +void widget_init(struct widget *widget, int lines_, int cols, int y, int x, + chtype bkgd, unsigned int flags) +{ + WINDOW *old_window; + + if (y == SCREEN_CENTER) + y = (screen_lines - lines_) / 2; + if (x == SCREEN_CENTER) + x = (screen_cols - cols) / 2; + + old_window = widget->window; + widget->window = newwin(lines_, cols, y, x); + if (!widget->window) + fatal_error("cannot create window"); + keypad(widget->window, TRUE); + nodelay(widget->window, TRUE); + leaveok(widget->window, !(flags & WIDGET_CURSOR_VISIBLE)); + wbkgdset(widget->window, bkgd); + werase(widget->window); + + if (flags & WIDGET_BORDER) + box(widget->window, 0, 0); + if (flags & WIDGET_SUBWINDOW) { + if (widget->subwindow) + delwin(widget->subwindow); + widget->subwindow = derwin(widget->window, + lines_ - 2, cols - 2, 1, 1); + if (!widget->subwindow) + fatal_error("cannot create subwindow"); + wbkgdset(widget->subwindow, bkgd); + } + widget->cursor_visibility = !!(flags & WIDGET_CURSOR_VISIBLE); + + if (widget->panel) { + replace_panel(widget->panel, widget->window); + } else { + widget->panel = new_panel(widget->window); + if (!widget->panel) + fatal_error("cannot create panel"); + set_panel_userptr(widget->panel, widget); + } + + if (old_window) + delwin(old_window); + + update_cursor_visibility(); +} + +void widget_free(struct widget *widget) +{ + if (widget->panel) { + del_panel(widget->panel); + widget->panel = NULL; + } + if (widget->subwindow) { + delwin(widget->subwindow); + widget->subwindow = NULL; + } + if (widget->window) { + delwin(widget->window); + widget->window = NULL; + } + + update_cursor_visibility(); +} + +const struct widget *get_active_widget(void) +{ + PANEL *active_panel; + + active_panel = panel_below(NULL); + if (active_panel) + return panel_userptr(active_panel); + else + return NULL; +} + +void window_size_changed(void) +{ + PANEL *panel, *below; + const struct widget *widget; + struct winsize size; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) == 0) + resize_term(size.ws_row, size.ws_col); + + getmaxyx(stdscr, screen_lines, screen_cols); + if (tigetflag("xenl") != 1 && tigetflag("am") != 1) + --screen_lines; + + for (panel = panel_below(NULL); panel; panel = below) { + below = panel_below(panel); + widget = panel_userptr(panel); + widget->window_size_changed(); + } +} diff --git a/alsamixer/widget.h b/alsamixer/widget.h new file mode 100644 index 0000000..58a132c --- /dev/null +++ b/alsamixer/widget.h @@ -0,0 +1,33 @@ +#ifndef WIDGET_H_INCLUDED +#define WIDGET_H_INCLUDED + +#include + +#define WIDGET_BORDER 0x1 +#define WIDGET_SUBWINDOW 0x2 +#define WIDGET_CURSOR_VISIBLE 0x4 + +#define SCREEN_CENTER -1 + +struct widget { + WINDOW *window; + WINDOW *subwindow; /* optional: contents without border */ + PANEL *panel; + int cursor_visibility; + + void (*handle_key)(int key); + void (*window_size_changed)(void); + void (*close)(void); +}; + +extern int screen_lines; +extern int screen_cols; + +void widget_init(struct widget *widget, + int lines_, int cols, int y, int x, + chtype bkgd, unsigned int flags); +void widget_free(struct widget *widget); +const struct widget *get_active_widget(void); +void window_size_changed(void); + +#endif diff --git a/alsaucm/Makefile.am b/alsaucm/Makefile.am new file mode 100644 index 0000000..2010fc0 --- /dev/null +++ b/alsaucm/Makefile.am @@ -0,0 +1,20 @@ +bin_PROGRAMS = \ + alsaucm + +if USE_RST2MAN +man_MANS = alsaucm.1 +endif + +alsaucm_SOURCES = usecase.c dump.c + +noinst_HEADERS = usecase.h + +AM_CPPFLAGS = \ + -Wall -I$(top_srcdir)/include + +%.1: %.rst + rst2man $< > $@ + +EXTRA_DIST = alsaucm.rst + +CLEANFILES = alsaucm.1 diff --git a/alsaucm/alsaucm.rst b/alsaucm/alsaucm.rst new file mode 100644 index 0000000..c4fe88d --- /dev/null +++ b/alsaucm/alsaucm.rst @@ -0,0 +1,235 @@ +========= + alsaucm +========= + +--------------------- +ALSA Use Case Manager +--------------------- + +:Author: Antonio Ospite +:Date: 2016-09-22 +:Copyright: GPLv2+ +:Manual section: 1 +:Manual group: General Commands Manual + +SYNOPSIS +======== + +*alsaucm* [command] + +DESCRIPTION +=========== + +alsaucm (ALSA Use Case Manager) is a program to use the ALSA `Use Case +Interface`_ from the command line. + +On complex sound cards, setting up audio routes is not trivial and mixer +settings can conflict one another preventing the audio card to work at all. + +The ALSA Use Case Manager is a mechanism for controlling complex audio +hardware establishing a relationship between hardware configurations and +meaningful use cases that the end-user can relate with. + +The use case manager can also be used to switch between use cases when +necessary, in a consistent way. + +At a lower level, the use case manager works by configuring the sound card +ALSA kcontrols to change the hardware digital and analog audio routing to +match the requested device use case. + +The use case manager kcontrol configurations are stored in easy to modify text +files. An audio use case can be defined by a **verb** and **device** parameter. + +The verb describes the use case action i.e. a phone call, listening to music, +recording a conversation etc. The device describes the physical audio capture +and playback hardware i.e. headphones, phone handset, bluetooth headset, etc. + + +OPTIONS +======= + +Available options: + + **-h**, **--help** + this help + + **-c**, **--card** `NAME` + open card NAME + + **-i**, **--interactive** + interactive mode + + **-b**, **--batch** `FILE` + batch mode (use ``'-'`` for the stdin input) + + **-n**, **--no-open** + do not open first card found + + +Available commands: + + ``open`` `NAME` + open card NAME. + + valid names are sound card names as listed in ``/usr/share/alsa/ucm``. + + ``reset`` + reset sound card to default state. + + ``reload`` + reload configuration. + + ``listcards`` + list available cards. + + ``list`` `IDENTIFIER` + list command, for items returning two entries (value+comment). + + the value of the `IDENTIFIER` argument can be: + + - ``_verbs`` - get verb list (in pair verb+comment) + - ``_devices[/{verb}]`` - get list of supported devices (in pair device+comment) + - ``_modifiers[/{verb}]`` - get list of supported modifiers (in pair modifier+comment) + + The forms without the trailing ``/{verb}`` are valid only after a specific + verb has been set. + + ``list1`` `IDENTIFIER` + list command, for lists returning one item per entry. + + the value of the `IDENTIFIER` argument can vary depending on the context, + it can be: + + - ``TQ[/{verb}]`` - get list of Tone Quality identifiers + - ``_enadevs`` - get list of enabled devices + - ``_enamods`` - get list of enabled modifiers + - ``_supporteddevs/{modifier}|{device}[/{verb}]`` - list of supported devices + - ``_conflictingdevs/{modifier}|{device}[/{verb}]`` - list of conflicting devices + + ``get`` `IDENTIFIER` + get string value. + + the value of the `IDENTIFIER` argument can be: + + - ``_verb`` - return current verb + - ``[=]{NAME}[/[{modifier}|{/device}][/{verb}]]`` (For valid NAMEs look at the + ALSA `Use Case Interface`_) + + + ``geti`` `IDENTIFIER` + get integer value. + + the value of the `IDENTIFIER` argument can be: + + - ``_devstatus/{device}`` + - ``_modstatus/{device}`` + + ``set`` `IDENTIFIER` `VALUE` + set string value + + The value of the `IDENTIFIER` argument can be: + + - ``_verb`` - set the verb to `VALUE` + - ``_enadev`` - enable the device specified by `VALUE` + - ``_disdev`` - disable the device specified by `VALUE` + - ``_swdev/{old_device}`` - switche device: + + - disable `old_device` and then enable the device specified by + `VALUE` + - if no device was enabled just return + + - ``_enamod`` - enable the modifier specified by `VALUE` + - ``_dismod`` - disable the modifier specified by `VALUE` + - ``_swmod/{old_modifier}`` - switch modifier: + + - disable `old_modifier` and then enable the modifier specified by + `VALUE` + - if no modifier was enabled just return + + Note that the identifiers referring to devices and modifiers are valid + only after setting a verb. + + ``h``, ``help`` + help + + ``q``, ``quit`` + quit + + +FILES +===== + +The master use case files for each supported sound card are in ``/usr/share/alsa/ucm``. + +For example, the master use case file for the `Pandaboard` card is in +``/usr/share/alsa/ucm/PandaBoard/PandaBoard.conf``, this file lists all the +supported use cases, e.g. + +:: + + SectionUseCase."HiFi" { + File "hifi" + Comment "Play HiFi quality Music." + } + ... + + +Each use case defines a _verb, which is described in the file specified in +the ``File`` directive, like above. + +The ``HiFi`` verb above is described in +``/usr/share/alsa/ucm/PandaBoard/hifi``. + +For more details on the syntax of UCM files, see the alsa-lib source code: +http://git.alsa-project.org/?p=alsa-lib.git;a=blob;f=src/ucm/parser.c + + +EXAMPLES OF USE +=============== + +Some commands, like for instance ``list _devices``, +can only work after setting a ``_verb`` in the **same execution**, for +instance this sequence doesn't work: + +:: + + # alsaucm -c bytcr-rt5640 set _verb HiFi + # alsaucm -c bytcr-rt5640 list _devices + + +However this command does: + +:: + + # alsaucm -n -b - < + */ + +#include +#include +#include +#include +#include "usecase.h" +#include "aconfig.h" +#include "version.h" + +struct renderer { + int (*init)(struct renderer *r); + void (*done)(struct renderer *r); + int (*verb_begin)(struct renderer *r, + const char *verb, + const char *comment); + int (*verb_end)(struct renderer *r); + int (*device_block_begin)(struct renderer *r); + int (*device_block_end)(struct renderer *r); + int (*device_begin)(struct renderer *r, + const char *device, + const char *comment); + int (*device_end)(struct renderer *r); + int (*modifier_block_begin)(struct renderer *r); + int (*modifier_block_end)(struct renderer *r); + int (*modifier_begin)(struct renderer *r, + const char *device, + const char *comment); + int (*modifier_end)(struct renderer *r); + int (*supported_begin)(struct renderer *r); + int (*supported_value)(struct renderer *r, const char *value, int last); + int (*supported_end)(struct renderer *r); + int (*conflict_begin)(struct renderer *r); + int (*conflict_value)(struct renderer *r, const char *value, int last); + int (*conflict_end)(struct renderer *r); + int (*value_begin)(struct renderer *r); + int (*value_end)(struct renderer *r); + int (*value)(struct renderer *r, const char *ident, const char *value); + void *opaque; +}; + +/* + * Text renderer + */ + +struct text { + char a[1]; +}; + +static char *tesc(const char *s, char *buf, size_t buf_len) +{ + char *dst = buf; + char c = '\0'; + if (strchr(s, '"') || strchr(s, ' ') || strchr(s, '.')) { + *dst++ = c = '"'; + buf_len--; + } + while (*s && buf_len > 2) { + if (*s == '"') { + if (buf_len > 3) { + *dst++ = '\\'; + *dst++ = *s++; + buf_len -= 2; + continue; + } else { + break; + } + } + *dst++ = *s++; + } + if (c) + *dst++ = c; + *dst = '\0'; + return buf; +} + +#define ESC(s, esc) tesc((s), (esc), sizeof(esc)) + +static int text_verb_start(struct renderer *r ATTRIBUTE_UNUSED, + const char *verb, const char *comment) +{ + char buf1[128], buf2[128]; + printf("Verb.%s {\n", ESC(verb, buf1)); + if (comment && comment[0]) + printf("\tComment %s\n", ESC(comment, buf2)); + return 0; +} + +static int text_verb_end(struct renderer *r ATTRIBUTE_UNUSED) +{ + printf("}\n"); + return 0; +} + +static int text_2nd_level_begin(struct renderer *r ATTRIBUTE_UNUSED, + const char *key, + const char *val, + const char *comment) +{ + char buf1[128], buf2[128]; + printf("\t%s.%s {\n", key, ESC(val, buf1)); + if (comment && comment[0]) + printf("\t\tComment %s\n", ESC(comment, buf2)); + return 0; +} + +static int text_2nd_level_end(struct renderer *r ATTRIBUTE_UNUSED) +{ + printf("\t}\n"); + return 0; +} + +static int text_2nd_level(struct renderer *r ATTRIBUTE_UNUSED, const char *txt) +{ + printf("\t\t%s", txt); + return 0; +} + +static int text_3rd_level(struct renderer *r ATTRIBUTE_UNUSED, const char *txt) +{ + printf("\t\t\t%s", txt); + return 0; +} + +static int text_dev_start(struct renderer *r, const char *dev, const char *comment) +{ + return text_2nd_level_begin(r, "Device", dev, comment); +} + +static int text_mod_start(struct renderer *r, const char *dev, const char *comment) +{ + return text_2nd_level_begin(r, "Modifier", dev, comment); +} + +static int text_supcon_start(struct renderer *r, const char *key) +{ + if (text_2nd_level(r, key)) + return 1; + printf(" [\n"); + return 0; +} + +static int text_supcon_value(struct renderer *r, const char *value, int last) +{ + char buf[256]; + ESC(value, buf); + if (!last && strlen(buf) < sizeof(buf) - 2) + strcat(buf, ","); + if (text_3rd_level(r, buf)) + return 1; + printf("\n"); + return 0; +} + +static int text_supcon_end(struct renderer *r) +{ + return text_2nd_level(r, "]\n"); +} + +static int text_sup_start(struct renderer *r) +{ + return text_supcon_start(r, "SupportedDevices"); +} + +static int text_con_start(struct renderer *r) +{ + return text_supcon_start(r, "ConflictingDevices"); +} + +static int text_value_begin(struct renderer *r) +{ + return text_2nd_level(r, "Values {\n"); +} + +static int text_value_end(struct renderer *r) +{ + return text_2nd_level(r, "}\n"); +} + +static int text_value(struct renderer *r, const char *ident, const char *value) +{ + char buf1[256], buf2[256]; + int err; + + ESC(ident, buf1); + err = text_3rd_level(r, buf1); + if (err < 0) + return err; + ESC(value, buf2); + printf(" %s\n", buf2); + return 0; +} + +static struct renderer text_renderer = { + .verb_begin = text_verb_start, + .verb_end = text_verb_end, + .device_begin = text_dev_start, + .device_end = text_2nd_level_end, + .modifier_begin = text_mod_start, + .modifier_end = text_2nd_level_end, + .supported_begin = text_sup_start, + .supported_value = text_supcon_value, + .supported_end = text_supcon_end, + .conflict_begin = text_con_start, + .conflict_value = text_supcon_value, + .conflict_end = text_supcon_end, + .value_begin = text_value_begin, + .value_end = text_value_end, + .value = text_value, +}; + +/* + * JSON renderer + */ + +struct json { + int block[5]; +}; + +static char *jesc(const char *s, char *buf, size_t buf_len) +{ + char *dst = buf; + char c = '"'; + *dst++ = c; + buf_len--; + while (*s && buf_len > 2) { + if (*s == '"') { + if (buf_len > 3) { + *dst++ = '\\'; + *dst++ = *s++; + buf_len -= 2; + continue; + } else { + break; + } + } + *dst++ = *s++; + } + *dst++ = c; + *dst = '\0'; + return buf; +} + +#define JESC(s, esc) jesc((s), (esc), sizeof(esc)) + +static void json_block(struct renderer *r, int level, int last) +{ + struct json *j = r->opaque; + printf((j->block[level] && !last) ? ",\n" : "\n"); + j->block[level] = last ? 0 : 1; +} + +static int json_init(struct renderer *r ATTRIBUTE_UNUSED) +{ + printf("{\n \"Verbs\": {"); + return 0; +} + +static void json_done(struct renderer *r) +{ + json_block(r, 0, 1); + printf(" }\n}\n"); +} + +static int json_verb_start(struct renderer *r, const char *verb, const char *comment) +{ + char buf[256]; + json_block(r, 0, 0); + printf(" %s: {", JESC(verb, buf)); + if (comment && comment[0]) { + json_block(r, 1, 0); + printf(" \"Comment\": %s", JESC(comment, buf)); + } + return 0; +} + +static int json_verb_end(struct renderer *r) +{ + json_block(r, 1, 1); + printf(" }"); + return 0; +} + +static int json_2nd_level_block_end(struct renderer *r) +{ + json_block(r, 2, 1); + printf(" }"); + return 0; +} + +static int json_2nd_level_begin(struct renderer *r, + const char *val, + const char *comment) +{ + char buf[256]; + json_block(r, 2, 0); + printf(" %s: {", JESC(val, buf)); + if (comment && comment[0]) { + json_block(r, 3, 0); + printf(" \"Comment\": %s", JESC(comment, buf)); + } + return 0; +} + +static int json_2nd_level_end(struct renderer *r) +{ + json_block(r, 3, 1); + printf(" }"); + return 0; +} + +static int json_2nd_level(struct renderer *r ATTRIBUTE_UNUSED, const char *txt) +{ + printf(" %s", txt); + return 0; +} + +static int json_3rd_level(struct renderer *r ATTRIBUTE_UNUSED, const char *txt) +{ + printf(" %s", txt); + return 0; +} + +static int json_dev_block_start(struct renderer *r) +{ + json_block(r, 1, 0); + printf(" \"Devices\": {"); + return 0; +} + +static int json_mod_block_start(struct renderer *r) +{ + json_block(r, 1, 0); + printf(" \"Modifiers\": {"); + return 0; +} + +static int json_supcon_start(struct renderer *r, const char *key) +{ + json_block(r, 3, 0); + if (json_2nd_level(r, key)) + return 1; + printf(": ["); + return 0; +} + +static int json_supcon_value(struct renderer *r, const char *value, + int last ATTRIBUTE_UNUSED) +{ + char buf[256]; + JESC(value, buf); + json_block(r, 4, 0); + return json_3rd_level(r, buf); +} + +static int json_supcon_end(struct renderer *r) +{ + json_block(r, 4, 1); + return json_2nd_level(r, "]"); +} + +static int json_sup_start(struct renderer *r) +{ + return json_supcon_start(r, "\"SupportedDevices\""); +} + +static int json_con_start(struct renderer *r) +{ + return json_supcon_start(r, "\"ConflictingDevices\""); +} + +static int json_value_begin(struct renderer *r) +{ + json_block(r, 3, 0); + return json_2nd_level(r, "\"Values\": {"); +} + +static int json_value_end(struct renderer *r) +{ + json_block(r, 4, 1); + return json_2nd_level(r, "}"); +} + +static int json_value(struct renderer *r, const char *ident, const char *value) +{ + char buf[256]; + int err; + + json_block(r, 4, 0); + JESC(ident, buf); + err = json_3rd_level(r, buf); + if (err < 0) + return err; + JESC(value, buf); + printf(": %s", buf); + return 0; +} + +static struct renderer json_renderer = { + .init = json_init, + .done = json_done, + .verb_begin = json_verb_start, + .verb_end = json_verb_end, + .device_block_begin = json_dev_block_start, + .device_block_end = json_2nd_level_block_end, + .device_begin = json_2nd_level_begin, + .device_end = json_2nd_level_end, + .modifier_block_begin = json_mod_block_start, + .modifier_block_end = json_2nd_level_block_end, + .modifier_begin = json_2nd_level_begin, + .modifier_end = json_2nd_level_end, + .supported_begin = json_sup_start, + .supported_value = json_supcon_value, + .supported_end = json_supcon_end, + .conflict_begin = json_con_start, + .conflict_value = json_supcon_value, + .conflict_end = json_supcon_end, + .value_begin = json_value_begin, + .value_end = json_value_end, + .value = json_value, +}; + +/* + * universal dump functions + */ + +static int render_devlist(struct context *context, + struct renderer *render, + const char *verb, + const char *device, + const char *list, + int (*begin)(struct renderer *), + int (*value)(struct renderer *, const char *value, int last), + int (*end)(struct renderer *)) +{ + snd_use_case_mgr_t *uc_mgr = context->uc_mgr; + const char **dev_list; + char buf[256]; + int err = 0, j, dev_num; + + snprintf(buf, sizeof(buf), "%s/%s/%s", list, device, verb); + dev_num = snd_use_case_get_list(uc_mgr, buf, &dev_list); + if (dev_num < 0) { + fprintf(stderr, "%s: unable to get %s for verb '%s' for device '%s'\n", + context->command, list, verb, device); + return dev_num; + } + if (dev_num > 0) { + err = begin(render); + if (err < 0) + goto __err; + for (j = 0; j < dev_num; j++) { + err = value(render, dev_list[j], j + 1 == dev_num); + if (err < 0) + goto __err; + } + err = end(render); + } +__err: + snd_use_case_free_list(dev_list, dev_num); + return err; +} + +static int render_values(struct context *context, + struct renderer *render, + const char *verb, + const char *device) +{ + snd_use_case_mgr_t *uc_mgr = context->uc_mgr; + const char **list, *value; + char buf[256]; + int err = 0, j, num; + + snprintf(buf, sizeof(buf), "_identifiers/%s/%s", device, verb); + num = snd_use_case_get_list(uc_mgr, buf, &list); + if (num < 0) { + fprintf(stderr, "%s: unable to get _identifiers for verb '%s' for device '%s': %s\n", + context->command, verb, device, snd_strerror(num)); + return num; + } + if (num == 0) + goto __err; + if (render->value_begin) { + err = render->value_begin(render); + if (err < 0) + goto __err; + } + for (j = 0; j < num; j++) { + snprintf(buf, sizeof(buf), "%s/%s/%s", list[j], device, verb); + err = snd_use_case_get(uc_mgr, buf, &value); + if (err < 0) { + fprintf(stderr, "%s: unable to get value '%s' for verb '%s' for device '%s': %s\n", + context->command, list[j], verb, device, snd_strerror(err)); + goto __err; + } + err = render->value(render, list[j], value); + free((char *)value); + if (err < 0) + goto __err; + } + if (render->value_end) + err = render->value_end(render); +__err: + snd_use_case_free_list(list, num); + return err; +} + +static int render_device(struct context *context, + struct renderer *render, + const char *verb, + const char *device) +{ + int err; + + err = render_devlist(context, render, verb, device, + "_supporteddevs", + render->supported_begin, + render->supported_value, + render->supported_end); + if (err < 0) + return err; + err = render_devlist(context, render, verb, device, + "_conflictingdevs", + render->conflict_begin, + render->conflict_value, + render->conflict_end); + if (err < 0) + return err; + return render_values(context, render, verb, device); +} + +static void render(struct context *context, struct renderer *render) +{ + snd_use_case_mgr_t *uc_mgr = context->uc_mgr; + int i, j, num, dev_num; + const char **list, **dev_list, *verb, *comment; + char buf[256]; + + num = snd_use_case_verb_list(uc_mgr, &list); + if (num < 0) { + fprintf(stderr, "%s: no verbs found\n", context->command); + return; + } + if (render->init && render->init(render)) + goto __end; + for (i = 0; i < num; i += 2) { + /* verb */ + verb = list[i + 0]; + comment = list[i + 1]; + if (render->verb_begin(render, verb, comment)) + break; + /* devices */ + snprintf(buf, sizeof(buf), "_devices/%s", verb); + dev_num = snd_use_case_get_list(uc_mgr, buf, &dev_list); + if (dev_num < 0) { + fprintf(stderr, "%s: unable to get devices for verb '%s'\n", + context->command, verb); + continue; + } + if (dev_num == 0) + goto __mods; + if (render->device_block_begin && render->device_block_begin(render)) { + snd_use_case_free_list(dev_list, dev_num); + goto __end; + } + for (j = 0; j < dev_num; j += 2) { + render->device_begin(render, dev_list[j + 0], dev_list[j + 1]); + if (render_device(context, render, verb, dev_list[j + 0])) { + snd_use_case_free_list(dev_list, dev_num); + goto __end; + } + render->device_end(render); + } + snd_use_case_free_list(dev_list, dev_num); + if (render->device_block_end && render->device_block_end(render)) + goto __end; +__mods: + /* modifiers */ + snprintf(buf, sizeof(buf), "_modifiers/%s", verb); + dev_num = snd_use_case_get_list(uc_mgr, buf, &dev_list); + if (dev_num < 0) { + fprintf(stderr, "%s: unable to get modifiers for verb '%s'\n", + context->command, verb); + continue; + } + if (dev_num == 0) + goto __verb_end; + if (render->modifier_block_begin && render->modifier_block_begin(render)) { + snd_use_case_free_list(dev_list, dev_num); + goto __end; + } + for (j = 0; j < dev_num; j += 2) { + render->modifier_begin(render, dev_list[j + 0], dev_list[j + 1]); + render->modifier_end(render); + } + snd_use_case_free_list(dev_list, dev_num); + if (render->modifier_block_end && render->modifier_block_end(render)) + goto __end; +__verb_end: + /* end */ + if (render->verb_end(render)) + break; + } + if (render->done) + render->done(render); +__end: + snd_use_case_free_list(list, num); +} + +void dump(struct context *context, const char *format) +{ + struct renderer r; + struct text t; + struct json j; + + r.opaque = NULL; + if (strcasecmp(format, "text") == 0 || + strcasecmp(format, "txt") == 0) { + memset(&t, 0, sizeof(t)); + r = text_renderer; + r.opaque = &t; + } else if (strcasecmp(format, "json") == 0) { + memset(&j, 0, sizeof(j)); + r = json_renderer; + r.opaque = &j; + } + if (r.opaque != NULL) { + render(context, &r); + return; + } + fprintf(stderr, "%s: unknown dump format '%s'\n", + context->command, format); +} diff --git a/alsaucm/go.sh b/alsaucm/go.sh new file mode 100755 index 0000000..80164cb --- /dev/null +++ b/alsaucm/go.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +#GDB="gdb --args" +#GDB="strace" +#GDB="valgrind --leak-check=yes --show-reachable=yes" +#GDB="perf stat" +PROG=./alsaucm +#PROG=/home/perex/git/pipewire/builddir/spa/plugins/alsa/spa-acp-tool +#PROG="$HOME/git/pulseaudio/build/src/daemon/pulseaudio -n -F $HOME/git/pulseaudio/build/src/daemon/default.pa -p $HOME/git/pulseaudio/build/src/modules/" +#PROG=pulseaudio + +#ALSA_CONFIG_UCM="$HOME/alsa/alsa-ucm-conf/ucm" \ +ALSA_CONFIG_UCM2="$HOME/alsa/alsa-ucm-conf/ucm2" \ +LD_PRELOAD="$HOME/alsa/alsa-lib/src/.libs/libasound.so" \ +$GDB $PROG "$@" diff --git a/alsaucm/usecase.c b/alsaucm/usecase.c new file mode 100644 index 0000000..0aea84c --- /dev/null +++ b/alsaucm/usecase.c @@ -0,0 +1,546 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Support for the verb/device/modifier core logic and API, + * command line tool and file parser was kindly sponsored by + * Texas Instruments Inc. + * Support for multiple active modifiers and devices, + * transition sequences, multiple client access and user defined use + * cases was kindly sponsored by Wolfson Microelectronics PLC. + * + * Copyright (C) 2008-2010 SlimLogic Ltd + * Copyright (C) 2010 Wolfson Microelectronics PLC + * Copyright (C) 2010 Texas Instruments Inc. + * Copyright (C) 2010 Red Hat Inc. + * Authors: Liam Girdwood + * Stefan Schmidt + * Justin Xu + * Jaroslav Kysela + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "usecase.h" +#include "aconfig.h" +#include "version.h" + +#define MAX_BUF 256 + +enum uc_cmd { + /* management */ + OM_UNKNOWN = 0, + OM_OPEN, + OM_RESET, + OM_RELOAD, + OM_LISTCARDS, + OM_DUMP, + OM_LIST2, + OM_LIST1, + + /* set/get */ + OM_SET, + OM_GET, + OM_GET_VAL, + OM_GETI, + OM_GETI_VAL, + + /* misc */ + OM_HELP, + OM_QUIT, +}; + +struct cmd { + int code; + int args; + unsigned int opencard:1; + const char *id; +}; + +static struct cmd cmds[] = { + { OM_OPEN, 1, 0, "open" }, + { OM_RESET, 0, 1, "reset" }, + { OM_RELOAD, 0, 1, "reload" }, + { OM_LISTCARDS, 0, 0, "listcards" }, + { OM_DUMP, 1, 1, "dump" }, + { OM_LIST1, 1, 1, "list1" }, + { OM_LIST2, 1, 1, "list" }, + { OM_SET, 2, 1, "set" }, + { OM_GET, 1, 1, "get" }, + { OM_GET_VAL, 1, 1, "getval" }, + { OM_GETI, 1, 1, "geti" }, + { OM_GETI_VAL, 1, 1, "getival" }, + { OM_DUMP, 1, 1, "dump" }, + { OM_HELP, 0, 0, "help" }, + { OM_QUIT, 0, 0, "quit" }, + { OM_HELP, 0, 0, "h" }, + { OM_HELP, 0, 0, "?" }, + { OM_QUIT, 0, 0, "q" }, + { OM_UNKNOWN, 0, 0, NULL } +}; + +static void dump_help(struct context *context) +{ + if (context->command) + printf("Usage: %s [command]\n", context->command); + printf( +"\nAvailable options:\n" +" -h,--help this help\n" +" -c,--card NAME open card NAME\n" +" -i,--interactive interactive mode\n" +" -b,--batch FILE batch mode (use '-' for the stdin input)\n" +" -n,--no-open do not open first card found\n" +"\nAvailable commands:\n" +" open NAME open card NAME\n" +" reset reset sound card to default state\n" +" reload reload configuration\n" +" listcards list available cards\n" +" dump FORMAT dump all config information (format: text,json)\n" +" list IDENTIFIER list command, for items with value + comment\n" +" list1 IDENTIFIER list command, for items without comments\n" +" get IDENTIFIER get string value\n" +" geti IDENTIFIER get integer value\n" +" set IDENTIFIER VALUE set string value\n" +" h,help help\n" +" q,quit quit\n" +); +} + +int is_long(const char *str) +{ + char *end; + if (!*str) + return 0; + errno = 0; + strtol(str, &end, 10); + if (errno) + return 0; + if (*end) + return 0; + return 1; +} + +static int parse_line(struct context *context, char *line) +{ + char *start, **nargv; + int c; + + context->argc = 0; + while (*line) { + while (*line && (*line == ' ' || *line == '\t' || + *line == '\n')) + line++; + c = *line; + if (c == '\0') + return 0; + if (c == '\"' || c == '\'') { + start = ++line; + while (*line && *line != c) + line++; + if (*line) { + *line = '\0'; + line++; + } + } else { + start = line; + while (*line && *line != ' ' && *line != '\t' && + *line != '\n') + line++; + if (*line) { + *line = '\0'; + line++; + } + } + if (start[0] == '\0' && context->argc == 0) + return 0; + if (context->argc + 1 >= context->arga) { + context->arga += 4; + nargv = realloc(context->argv, + context->arga * sizeof(char *)); + if (nargv == NULL) + return -ENOMEM; + context->argv = nargv; + } + context->argv[context->argc++] = start; + } + return 0; +} + +static void my_exit(struct context *context, int exitcode) +{ + if (context->uc_mgr) + snd_use_case_mgr_close(context->uc_mgr); + if (context->arga > 0) + free(context->argv); + if (context->card) + free(context->card); + if (context->batch) + free(context->batch); + free(context); + snd_config_update_free_global(); + exit(exitcode); +} +static void do_initial_open(struct context *context) +{ + int card, err; + char name[16]; + + if (!context->no_open && context->card == NULL) { + card = -1; + err = snd_card_next(&card); + if (err < 0) { + fprintf(stderr, "%s: no sound card found: %s\n", + context->command, snd_strerror(err)); + my_exit(context, EXIT_FAILURE); + } + snprintf(name, sizeof(name), "hw:%d", card); + context->card = strdup(name); + } + + /* open library */ + if (!context->no_open) { + if (is_long(context->card)) { + snprintf(name, sizeof(name), "hw:%s", context->card); + free(context->card); + context->card = strdup(name); + } + if (context->card == NULL) { + fprintf(stderr, "%s: empty card name\n", context->command); + my_exit(context, EXIT_FAILURE); + } + err = snd_use_case_mgr_open(&context->uc_mgr, + context->card); + if (err < 0) { + fprintf(stderr, + "%s: error failed to open sound card %s: %s\n", + context->command, context->card, snd_strerror(err)); + my_exit(context, EXIT_FAILURE); + } + } +} + +static int do_one(struct context *context, struct cmd *cmd, char **argv) +{ + const char **list, *str; + long lval; + int err, i, j, entries; + + if (cmd->opencard && context->uc_mgr == NULL) { + if (!context->no_open) { + do_initial_open(context); + context->no_open = 1; + } else { + fprintf(stderr, "%s: command '%s' requires an open card\n", + context->command, cmd->id); + return 0; + } + } + switch (cmd->code) { + case OM_OPEN: + if (context->uc_mgr) + snd_use_case_mgr_close(context->uc_mgr); + context->uc_mgr = NULL; + free(context->card); + context->card = strdup(argv[0]); + err = snd_use_case_mgr_open(&context->uc_mgr, context->card); + if (err < 0) { + fprintf(stderr, + "%s: error failed to open sound card %s: %s\n", + context->command, context->card, + snd_strerror(err)); + return err; + } + break; + case OM_RESET: + err = snd_use_case_mgr_reset(context->uc_mgr); + if (err < 0) { + fprintf(stderr, + "%s: error failed to reset sound card %s: %s\n", + context->command, context->card, + snd_strerror(err)); + return err; + } + break; + case OM_RELOAD: + err = snd_use_case_mgr_reload(context->uc_mgr); + if (err < 0) { + fprintf(stderr, + "%s: error failed to reload manager %s: %s\n", + context->command, context->card, + snd_strerror(err)); + return err; + } + break; + case OM_LISTCARDS: + err = snd_use_case_card_list(&list); + if (err < 0) { + fprintf(stderr, + "%s: error failed to get card list: %s\n", + context->command, + snd_strerror(err)); + return err; + } + if (err == 0) { + printf(" list is empty\n"); + return 0; + } + for (i = 0; i < err / 2; i++) { + printf(" %i: %s\n", i, list[i*2]); + if (list[i*2+1]) + printf(" %s\n", list[i*2+1]); + } + snd_use_case_free_list(list, err); + break; + case OM_DUMP: + dump(context, argv[0]); + break; + case OM_LIST1: + case OM_LIST2: + switch (cmd->code) { + case OM_LIST1: + entries = 1; + break; + case OM_LIST2: + entries = 2; + break; + } + + err = snd_use_case_get_list(context->uc_mgr, + argv[0], + &list); + if (err < 0) { + fprintf(stderr, + "%s: error failed to get list %s: %s\n", + context->command, argv[0], + snd_strerror(err)); + return err; + } + if (err == 0) { + printf(" list is empty\n"); + return 0; + } + for (i = 0; i < err / entries; i++) { + printf(" %i: %s\n", i, list[i*entries]); + for (j = 0; j < entries - 1; j++) + if (list[i*entries+j+1]) + printf(" %s\n", list[i*entries+j+1]); + } + snd_use_case_free_list(list, err); + break; + case OM_SET: + err = snd_use_case_set(context->uc_mgr, argv[0], argv[1]); + if (err < 0) { + fprintf(stderr, + "%s: error failed to set %s=%s: %s\n", + context->command, argv[0], argv[1], + snd_strerror(err)); + return err; + } + break; + case OM_GET: + case OM_GET_VAL: + err = snd_use_case_get(context->uc_mgr, argv[0], &str); + if (err < 0) { + fprintf(stderr, + "%s: error failed to get %s: %s\n", + context->command, argv[0], + snd_strerror(err)); + return err; + } + if (cmd->code == OM_GET) + printf(" %s=%s\n", argv[0], str); + else + printf("%s\n", str); + free((void *)str); + break; + case OM_GETI: + case OM_GETI_VAL: + err = snd_use_case_geti(context->uc_mgr, argv[0], &lval); + if (err < 0) { + fprintf(stderr, + "%s: error failed to get integer %s: %s\n", + context->command, argv[0], + snd_strerror(err)); + return lval; + } + if (cmd->code == OM_GETI) + printf(" %s=%li\n", argv[0], lval); + else + printf("%li\n", lval); + break; + case OM_QUIT: + context->do_exit = 1; + break; + case OM_HELP: + dump_help(context); + break; + default: + fprintf(stderr, "%s: unimplemented command '%s'\n", + context->command, cmd->id); + return -EINVAL; + } + return 0; +} + +static int do_commands(struct context *context) +{ + char *command, **argv; + struct cmd *cmd; + int i, acnt, err; + + for (i = 0; i < context->argc && !context->do_exit; i++) { + command = context->argv[i]; + for (cmd = cmds; cmd->id != NULL; cmd++) { + if (strcmp(cmd->id, command) == 0) + break; + } + if (cmd->id == NULL) { + fprintf(stderr, "%s: unknown command '%s'\n", + context->command, command); + return -EINVAL; + } + acnt = context->argc - (i + 1); + if (acnt < cmd->args) { + fprintf(stderr, "%s: expected %i arguments (got %i)\n", + context->command, cmd->args, acnt); + return -EINVAL; + } + argv = context->argv + i + 1; + err = do_one(context, cmd, argv); + if (err < 0) + return err; + i += cmd->args; + } + return 0; +} + +enum { + OPT_VERSION = 1, +}; + +int main(int argc, char *argv[]) +{ + static const char short_options[] = "hb:c:in"; + static const struct option long_options[] = { + {"help", 0, 0, 'h'}, + {"version", 0, 0, OPT_VERSION}, + {"card", 1, 0, 'c'}, + {"interactive", 0, 0, 'i'}, + {"batch", 1, 0, 'b'}, + {"no-open", 0, 0, 'n'}, + {0, 0, 0, 0} + }; + struct context *context; + const char *command = argv[0]; + int c, err, option_index; + char cmd[MAX_BUF]; + FILE *in; + + context = calloc(1, sizeof(*context)); + if (context == NULL) + return EXIT_FAILURE; + context->command = command; + while ((c = getopt_long(argc, argv, short_options, + long_options, &option_index)) != -1) { + switch (c) { + case 'h': + dump_help(context); + break; + case OPT_VERSION: + printf("%s: version " SND_UTIL_VERSION_STR "\n", command); + break; + case 'c': + if (context->card) + free(context->card); + context->card = strdup(optarg); + break; + case 'i': + context->interactive = 1; + context->batch = NULL; + break; + case 'b': + context->batch = strdup(optarg); + context->interactive = 0; + break; + case 'n': + context->no_open = 1; + break; + default: + fprintf(stderr, "Try '%s --help' for more information.\n", command); + my_exit(context, EXIT_FAILURE); + } + } + + /* parse and execute any command line commands */ + if (argc > optind) { + context->argv = argv + optind; + context->argc = argc - optind; + err = do_commands(context); + if (err < 0) + my_exit(context, EXIT_FAILURE); + } + + if (!context->interactive && !context->batch) + my_exit(context, EXIT_SUCCESS); + + if (context->interactive) { + printf("%s: Interacive mode - 'q' to quit\n", command); + in = stdin; + } else { + if (strcmp(context->batch, "-") == 0) { + in = stdin; + } else { + in = fopen(context->batch, "r"); + if (in == NULL) { + fprintf(stderr, "%s: error failed to open file '%s': %s\n", + command, context->batch, strerror(errno)); + my_exit(context, EXIT_FAILURE); + } + } + } + + /* run the interactive command parser and handler */ + while (!context->do_exit && !feof(in)) { + if (context->interactive) + printf("%s>> ", argv[0]); + fflush(stdin); + if (fgets(cmd, MAX_BUF, in) == NULL) + break; + err = parse_line(context, cmd); + if (err < 0) { + fprintf(stderr, "%s: unable to parse line\n", + command); + my_exit(context, EXIT_FAILURE); + } + err = do_commands(context); + if (err < 0) { + if (context->interactive) + printf("^^^ error, try again\n"); + else + my_exit(context, EXIT_FAILURE); + } + } + + if (in != stdin) + fclose(in); + + my_exit(context, EXIT_SUCCESS); + return EXIT_SUCCESS; +} diff --git a/alsaucm/usecase.h b/alsaucm/usecase.h new file mode 100644 index 0000000..a85716a --- /dev/null +++ b/alsaucm/usecase.h @@ -0,0 +1,35 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __USECASE_H +#define __USECASE_H + +struct context { + snd_use_case_mgr_t *uc_mgr; + const char *command; + char *card; + char **argv; + int argc; + int arga; + char *batch; + unsigned int interactive:1; + unsigned int no_open:1; + unsigned int do_exit:1; +}; + +void dump(struct context *context, const char *format); + +#endif diff --git a/amidi/Makefile.am b/amidi/Makefile.am new file mode 100644 index 0000000..0b91685 --- /dev/null +++ b/amidi/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = amidi.1 + +bin_PROGRAMS = amidi +man_MANS = amidi.1 diff --git a/amidi/amidi.1 b/amidi/amidi.1 new file mode 100644 index 0000000..5bc24ba --- /dev/null +++ b/amidi/amidi.1 @@ -0,0 +1,178 @@ +.TH AMIDI 1 "30 Aug 2016" + +.SH NAME +amidi \- read from and write to ALSA RawMIDI ports + +.SH SYNOPSIS +\fBamidi\fP [\fI\-p port\fP] [\fI\-s file\fP | \fI\-S data\fP] +[\fI\-r file\fP] [\fI\-d\fP] [\fI\-t seconds\fP] [\fI\-a\fP] + +.SH DESCRIPTION +.B amidi +is a command-line utility which allows one to receive and send +SysEx (system exclusive) data from/to external MIDI devices. +It can also send any other MIDI commands. + +.B amidi +handles only files containing raw MIDI commands, without timing +information. +.B amidi +does not support Standard MIDI (.mid) files, but +.B aplaymidi(1) +and +.B arecordmidi(1) +do. + +.SH OPTIONS + +Use the +.I \-h, +.I \-V, +.I \-l, +or +.I \-L +options to display information; +or use at least one of the +.I \-s, +.I \-r, +.I \-S, +or +.I \-d +options to specify what data to send or receive. + +.TP +.I \-h, \-\-help +Help: prints a list of options. + +.TP +.I \-V, \-\-version +Prints the current version. + +.TP +.I \-l, \-\-list\-devices +Prints a list of all hardware MIDI ports. + +.TP +.I \-L, \-\-list\-rawmidis +Prints all RawMIDI definitions. +(used when debugging configuration files) + +.TP +.I \-p, \-\-port=name +Sets the name of the ALSA RawMIDI port to use. +If this is not specified, +.B amidi +uses the default port defined in the configuration file +(the default for this is port 0 on card 0, which may not exist). + +.TP +.I \-s, \-\-send=filename +Sends the contents of the specified file to the MIDI port. +The file must contain raw MIDI commands (e.g. a .syx file); +for Standard MIDI (.mid) files, use +.B aplaymidi(1). + +.TP +.I \-r, \-\-receive=filename +Writes data received from the MIDI port into the specified file. +The file will contain raw MIDI commands (such as in a .syx file); +to record a Standard MIDI (.mid) file, use +.B arecordmidi(1). + +.B amidi +will filter out any Active Sensing and Clock bytes (FEh, F8h), unless the +.I \-a +or +.I \-c +options have been given. + +.TP +.I \-S, \-\-send\-hex="..." +Sends the bytes specified as hexadecimal numbers to the MIDI port. + +.TP +.I \-d, \-\-dump +Prints data received from the MIDI port as hexadecimal bytes. +Active Sensing and Clock bytes (FEh, F8h) will not be shown, unless the +.I \-a +or +.I \-c +options have been given. + +This option is useful for debugging. + +.TP +.I \-t, \-\-timeout=seconds +Stops receiving data when no data has been received for the specified +amount of time. + +If this option has not been given, you must press Ctrl+C (or kill +.B amidi\fR) +to stop receiving data. + +.TP +.I \-a, \-\-active\-sensing +Does not ignore Active Sensing bytes (FEh) when saving or printing +received MIDI commands. + +.TP +.I \-c, \-\-clock +Does not ignore Clock bytes (F8h) when saving or printing received +MIDI commands. + +.TP +.I \-i, \-\-sysex-interval=mseconds +Adds a delay in between each SysEx message sent to a device. It is +useful when sending firmware updates via SysEx messages to a remote +device. + +.SH EXAMPLES + +.TP +.B amidi \-p hw:0 \-s my_settings.syx +will send the MIDI commands in +.I my_settings.syx +to port +.I hw:0. + +.TP +.B amidi \-p hw:1,0,0 -s firmware.syx \-i 100 +will send the MIDI commands in +.I firmware.syx +to port +.I hw:1,0,0 +with 100 milliseconds delay in between each SysEx message. + +.TP +.B amidi \-S 'F0 43 10 4C 00 00 7E 00 F7' +sends an XG Reset to the default port. + +.TP +.B amidi \-p hw:1,2 \-S F0411042110C000000000074F7 \-r dump.syx \-t 1 +sends a \(lqParameter Dump Request\(rq to a GS device, saves the received +parameter data to the file +.I dump.syx, +and stops after the device has finished sending data +(when no data has been received for one second). + +.TP +.B amidi \-p virtual \-d +creates a virtual RawMIDI port and prints all data sent to this port. + +.SH FILES +.I /usr/share/alsa/alsa.conf +default rawmidi definitions +.br +.I /etc/asound.conf +system\-wide rawmidi definitions +.br +.I ~/.asoundrc +user specific rawmidi definitions + +.SH SEE ALSO +aplaymidi(1) +.br +arecordmidi(1) + +.SH AUTHOR +Clemens Ladisch diff --git a/amidi/amidi.c b/amidi/amidi.c new file mode 100644 index 0000000..75fb8c0 --- /dev/null +++ b/amidi/amidi.c @@ -0,0 +1,763 @@ +/* + * amidi.c - read from/write to RawMIDI ports + * + * Copyright (c) Clemens Ladisch + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define _GNU_SOURCE +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NSEC_PER_SEC 1000000000L + +static int do_print_timestamp = 0; +static int do_device_list, do_rawmidi_list; +static char *port_name = "default"; +static char *send_file_name; +static char *receive_file_name; +static char *send_hex; +static char *send_data; +static int send_data_length; +static int receive_file; +static int dump; +static float timeout; +static int stop; +static int sysex_interval; +static snd_rawmidi_t *input, **inputp; +static snd_rawmidi_t *output, **outputp; + +static void error(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vfprintf(stderr, format, ap); + va_end(ap); + putc('\n', stderr); +} + +static void usage(void) +{ + printf( + "Usage: amidi options\n" + "\n" + "-h, --help this help\n" + "-V, --version print current version\n" + "-l, --list-devices list all hardware ports\n" + "-L, --list-rawmidis list all RawMIDI definitions\n" + "-p, --port=name select port by name\n" + "-s, --send=file send the contents of a (.syx) file\n" + "-r, --receive=file write received data into a file\n" + "-S, --send-hex=\"...\" send hexadecimal bytes\n" + "-d, --dump print received data as hexadecimal bytes\n" + "-T, --timestamp=... adds a timestamp in front of each dumped message\n" + " realtime\n" + " monotonic\n" +#ifdef CLOCK_MONOTONIC_RAW + " raw\n" +#endif + "-t, --timeout=seconds exits when no data has been received\n" + " for the specified duration\n" + "-a, --active-sensing include active sensing bytes\n" + "-c, --clock include clock bytes\n" + "-i, --sysex-interval=mseconds delay in between each SysEx message\n"); +} + +static void version(void) +{ + puts("amidi version " SND_UTIL_VERSION_STR); +} + +static void *my_malloc(size_t size) +{ + void *p = malloc(size); + if (!p) { + error("out of memory"); + exit(EXIT_FAILURE); + } + return p; +} + +static void list_device(snd_ctl_t *ctl, int card, int device) +{ + snd_rawmidi_info_t *info; + const char *name; + const char *sub_name; + int subs, subs_in, subs_out; + int sub; + int err; + + snd_rawmidi_info_alloca(&info); + snd_rawmidi_info_set_device(info, device); + + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); + err = snd_ctl_rawmidi_info(ctl, info); + if (err >= 0) + subs_in = snd_rawmidi_info_get_subdevices_count(info); + else + subs_in = 0; + + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT); + err = snd_ctl_rawmidi_info(ctl, info); + if (err >= 0) + subs_out = snd_rawmidi_info_get_subdevices_count(info); + else + subs_out = 0; + + subs = subs_in > subs_out ? subs_in : subs_out; + if (!subs) + return; + + for (sub = 0; sub < subs; ++sub) { + snd_rawmidi_info_set_stream(info, sub < subs_in ? + SND_RAWMIDI_STREAM_INPUT : + SND_RAWMIDI_STREAM_OUTPUT); + snd_rawmidi_info_set_subdevice(info, sub); + err = snd_ctl_rawmidi_info(ctl, info); + if (err < 0) { + error("cannot get rawmidi information %d:%d:%d: %s\n", + card, device, sub, snd_strerror(err)); + return; + } + name = snd_rawmidi_info_get_name(info); + sub_name = snd_rawmidi_info_get_subdevice_name(info); + if (sub == 0 && sub_name[0] == '\0') { + printf("%c%c hw:%d,%d %s", + sub < subs_in ? 'I' : ' ', + sub < subs_out ? 'O' : ' ', + card, device, name); + if (subs > 1) + printf(" (%d subdevices)", subs); + putchar('\n'); + break; + } else { + printf("%c%c hw:%d,%d,%d %s\n", + sub < subs_in ? 'I' : ' ', + sub < subs_out ? 'O' : ' ', + card, device, sub, sub_name); + } + } +} + +static void list_card_devices(int card) +{ + snd_ctl_t *ctl; + char name[32]; + int device; + int err; + + sprintf(name, "hw:%d", card); + if ((err = snd_ctl_open(&ctl, name, 0)) < 0) { + error("cannot open control for card %d: %s", card, snd_strerror(err)); + return; + } + device = -1; + for (;;) { + if ((err = snd_ctl_rawmidi_next_device(ctl, &device)) < 0) { + error("cannot determine device number: %s", snd_strerror(err)); + break; + } + if (device < 0) + break; + list_device(ctl, card, device); + } + snd_ctl_close(ctl); +} + +static void device_list(void) +{ + int card, err; + + card = -1; + if ((err = snd_card_next(&card)) < 0) { + error("cannot determine card number: %s", snd_strerror(err)); + return; + } + if (card < 0) { + error("no sound card found"); + return; + } + puts("Dir Device Name"); + do { + list_card_devices(card); + if ((err = snd_card_next(&card)) < 0) { + error("cannot determine card number: %s", snd_strerror(err)); + break; + } + } while (card >= 0); +} + +static void rawmidi_list(void) +{ + snd_output_t *output; + snd_config_t *config; + int err; + + if ((err = snd_config_update()) < 0) { + error("snd_config_update failed: %s", snd_strerror(err)); + return; + } + if ((err = snd_output_stdio_attach(&output, stdout, 0)) < 0) { + error("snd_output_stdio_attach failed: %s", snd_strerror(err)); + return; + } + if (snd_config_search(snd_config, "rawmidi", &config) >= 0) { + puts("RawMIDI list:"); + snd_config_save(config, output); + } + snd_output_close(output); +} + +static int send_midi_interleaved(void) +{ + int err; + char *data = send_data; + size_t buffer_size; + snd_rawmidi_params_t *param; + snd_rawmidi_status_t *st; + + snd_rawmidi_status_alloca(&st); + + snd_rawmidi_params_alloca(¶m); + snd_rawmidi_params_current(output, param); + buffer_size = snd_rawmidi_params_get_buffer_size(param); + + while (data < (send_data + send_data_length)) { + int len = send_data + send_data_length - data; + char *temp; + + if (data > send_data) { + snd_rawmidi_status(output, st); + do { + /* 320 µs per byte as noted in Page 1 of MIDI spec */ + usleep((buffer_size - snd_rawmidi_status_get_avail(st)) * 320); + snd_rawmidi_status(output, st); + } while(snd_rawmidi_status_get_avail(st) < buffer_size); + usleep(sysex_interval * 1000); + } + + /* find end of SysEx */ + if ((temp = memchr(data, 0xf7, len)) != NULL) + len = temp - data + 1; + + if ((err = snd_rawmidi_write(output, data, len)) < 0) + return err; + + data += len; + } + + return 0; +} + +static void load_file(void) +{ + int fd; + off_t length; + + fd = open(send_file_name, O_RDONLY); + if (fd == -1) { + error("cannot open %s - %s", send_file_name, strerror(errno)); + return; + } + length = lseek(fd, 0, SEEK_END); + if (length == (off_t)-1) { + error("cannot determine length of %s: %s", send_file_name, strerror(errno)); + goto _error; + } + send_data = my_malloc(length); + lseek(fd, 0, SEEK_SET); + if (read(fd, send_data, length) != length) { + error("cannot read from %s: %s", send_file_name, strerror(errno)); + goto _error; + } + if (length >= 4 && !memcmp(send_data, "MThd", 4)) { + error("%s is a Standard MIDI File; use aplaymidi to send it", send_file_name); + goto _error; + } + send_data_length = length; + goto _exit; +_error: + free(send_data); + send_data = NULL; +_exit: + close(fd); +} + +static int hex_value(char c) +{ + if ('0' <= c && c <= '9') + return c - '0'; + if ('A' <= c && c <= 'F') + return c - 'A' + 10; + if ('a' <= c && c <= 'f') + return c - 'a' + 10; + error("invalid character %c", c); + return -1; +} + +static void parse_data(void) +{ + const char *p; + int i, value; + + send_data = my_malloc(strlen(send_hex)); /* guesstimate */ + i = 0; + value = -1; /* value is >= 0 when the first hex digit of a byte has been read */ + for (p = send_hex; *p; ++p) { + int digit; + if (isspace((unsigned char)*p)) { + if (value >= 0) { + send_data[i++] = value; + value = -1; + } + continue; + } + digit = hex_value(*p); + if (digit < 0) { + send_data = NULL; + return; + } + if (value < 0) { + value = digit; + } else { + send_data[i++] = (value << 4) | digit; + value = -1; + } + } + if (value >= 0) + send_data[i++] = value; + send_data_length = i; +} + +/* + * prints MIDI commands, formatting them nicely + */ +static void print_byte(unsigned char byte, struct timespec *ts) +{ + static enum { + STATE_UNKNOWN, + STATE_1PARAM, + STATE_1PARAM_CONTINUE, + STATE_2PARAM_1, + STATE_2PARAM_2, + STATE_2PARAM_1_CONTINUE, + STATE_SYSEX + } state = STATE_UNKNOWN; + int newline = 0; + + if (byte >= 0xf8) + newline = 1; + else if (byte >= 0xf0) { + newline = 1; + switch (byte) { + case 0xf0: + state = STATE_SYSEX; + break; + case 0xf1: + case 0xf3: + state = STATE_1PARAM; + break; + case 0xf2: + state = STATE_2PARAM_1; + break; + case 0xf4: + case 0xf5: + case 0xf6: + state = STATE_UNKNOWN; + break; + case 0xf7: + newline = state != STATE_SYSEX; + state = STATE_UNKNOWN; + break; + } + } else if (byte >= 0x80) { + newline = 1; + if (byte >= 0xc0 && byte <= 0xdf) + state = STATE_1PARAM; + else + state = STATE_2PARAM_1; + } else /* b < 0x80 */ { + int running_status = 0; + newline = state == STATE_UNKNOWN; + switch (state) { + case STATE_1PARAM: + state = STATE_1PARAM_CONTINUE; + break; + case STATE_1PARAM_CONTINUE: + running_status = 1; + break; + case STATE_2PARAM_1: + state = STATE_2PARAM_2; + break; + case STATE_2PARAM_2: + state = STATE_2PARAM_1_CONTINUE; + break; + case STATE_2PARAM_1_CONTINUE: + running_status = 1; + state = STATE_2PARAM_2; + break; + default: + break; + } + if (running_status) + fputs("\n ", stdout); + } + + putchar(newline ? '\n' : ' '); + if (newline && do_print_timestamp) { + /* Nanoseconds does not make a lot of sense for serial MIDI (the + * 31250 bps one) but I'm not sure about MIDI over USB. + */ + printf("%lld.%.9ld) ", (long long)ts->tv_sec, ts->tv_nsec); + } + + printf("%02X", byte); +} + +static void sig_handler(int sig ATTRIBUTE_UNUSED) +{ + stop = 1; +} + +static void add_send_hex_data(const char *str) +{ + int length; + char *s; + + length = (send_hex ? strlen(send_hex) + 1 : 0) + strlen(str) + 1; + s = my_malloc(length); + if (send_hex) { + strcpy(s, send_hex); + strcat(s, " "); + } else { + s[0] = '\0'; + } + strcat(s, str); + free(send_hex); + send_hex = s; +} + +int main(int argc, char *argv[]) +{ + static const char short_options[] = "hVlLp:s:r:S::dt:aci:T:"; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"list-devices", 0, NULL, 'l'}, + {"list-rawmidis", 0, NULL, 'L'}, + {"port", 1, NULL, 'p'}, + {"send", 1, NULL, 's'}, + {"receive", 1, NULL, 'r'}, + {"send-hex", 2, NULL, 'S'}, + {"dump", 0, NULL, 'd'}, + {"timestamp", 1, NULL, 'T'}, + {"timeout", 1, NULL, 't'}, + {"active-sensing", 0, NULL, 'a'}, + {"clock", 0, NULL, 'c'}, + {"sysex-interval", 1, NULL, 'i'}, + {0} + }; + int c, err, ok = 0; + int ignore_active_sensing = 1; + int ignore_clock = 1; + int do_send_hex = 0; + clockid_t cid = CLOCK_REALTIME; + struct itimerspec itimerspec = { .it_interval = { 0, 0 } }; + + while ((c = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage(); + return 0; + case 'V': + version(); + return 0; + case 'l': + do_device_list = 1; + break; + case 'L': + do_rawmidi_list = 1; + break; + case 'p': + port_name = optarg; + break; + case 's': + send_file_name = optarg; + break; + case 'r': + receive_file_name = optarg; + break; + case 'S': + do_send_hex = 1; + if (optarg) + add_send_hex_data(optarg); + break; + case 'd': + dump = 1; + break; + case 'T': + do_print_timestamp = 1; + if (optarg == NULL) + error("Clock type missing"); + else if (strcasecmp(optarg, "realtime") == 0) + cid = CLOCK_REALTIME; + else if (strcasecmp(optarg, "monotonic") == 0) + cid = CLOCK_MONOTONIC; +#ifdef CLOCK_MONOTONIC_RAW + else if (strcasecmp(optarg, "raw") == 0) + cid = CLOCK_MONOTONIC_RAW; +#endif + else + error("Clock type not known"); + break; + case 't': + if (optarg) + timeout = atof(optarg); + break; + case 'a': + ignore_active_sensing = 0; + break; + case 'c': + ignore_clock = 0; + break; + case 'i': + sysex_interval = atoi(optarg); + break; + default: + error("Try `amidi --help' for more information."); + return 1; + } + } + if (do_send_hex) { + /* data for -S can be specified as multiple arguments */ + if (!send_hex && !argv[optind]) { + error("Please specify some data for --send-hex."); + return 1; + } + for (; argv[optind]; ++optind) + add_send_hex_data(argv[optind]); + } else { + if (argv[optind]) { + error("%s is not an option.", argv[optind]); + return 1; + } + } + + if (do_rawmidi_list) + rawmidi_list(); + if (do_device_list) + device_list(); + if (do_rawmidi_list || do_device_list) + return 0; + + if (!send_file_name && !receive_file_name && !send_hex && !dump) { + error("Please specify at least one of --send, --receive, --send-hex, or --dump."); + return 1; + } + if (send_file_name && send_hex) { + error("--send and --send-hex cannot be specified at the same time."); + return 1; + } + + if (send_file_name) + load_file(); + else if (send_hex) + parse_data(); + if ((send_file_name || send_hex) && !send_data) + return 1; + + if (receive_file_name) { + receive_file = creat(receive_file_name, 0666); + if (receive_file == -1) { + error("cannot create %s: %s", receive_file_name, strerror(errno)); + return -1; + } + } else { + receive_file = -1; + } + + if (receive_file_name || dump) + inputp = &input; + else + inputp = NULL; + if (send_data) + outputp = &output; + else + outputp = NULL; + + if ((err = snd_rawmidi_open(inputp, outputp, port_name, SND_RAWMIDI_NONBLOCK)) < 0) { + error("cannot open port \"%s\": %s", port_name, snd_strerror(err)); + goto _exit2; + } + + if (inputp) + snd_rawmidi_read(input, NULL, 0); /* trigger reading */ + + if (send_data) { + if ((err = snd_rawmidi_nonblock(output, 0)) < 0) { + error("cannot set blocking mode: %s", snd_strerror(err)); + goto _exit; + } + if (!sysex_interval) { + if ((err = snd_rawmidi_write(output, send_data, send_data_length)) < 0) { + error("cannot send data: %s", snd_strerror(err)); + return err; + } + } else { + if ((err = send_midi_interleaved()) < 0) { + error("cannot send data: %s", snd_strerror(err)); + return err; + } + } + } + + if (inputp) { + int read = 0; + int npfds; + struct pollfd *pfds; + + npfds = 1 + snd_rawmidi_poll_descriptors_count(input); + pfds = alloca(npfds * sizeof(struct pollfd)); + + if (timeout > 0) { + pfds[0].fd = timerfd_create(CLOCK_MONOTONIC, 0); + if (pfds[0].fd == -1) { + error("cannot create timer: %s", strerror(errno)); + goto _exit; + } + pfds[0].events = POLLIN; + } else { + pfds[0].fd = -1; + } + + snd_rawmidi_poll_descriptors(input, &pfds[1], npfds - 1); + + signal(SIGINT, sig_handler); + + if (timeout > 0) { + float timeout_int; + + itimerspec.it_value.tv_nsec = modff(timeout, &timeout_int) * NSEC_PER_SEC; + itimerspec.it_value.tv_sec = timeout_int; + err = timerfd_settime(pfds[0].fd, 0, &itimerspec, NULL); + if (err < 0) { + error("cannot set timer: %s", strerror(errno)); + goto _exit; + } + } + + for (;;) { + unsigned char buf[256]; + int i, length; + unsigned short revents; + struct timespec ts; + + err = poll(pfds, npfds, -1); + if (stop || (err < 0 && errno == EINTR)) + break; + if (err < 0) { + error("poll failed: %s", strerror(errno)); + break; + } + + if (clock_gettime(cid, &ts) < 0) { + error("clock_getres (%d) failed: %s", cid, strerror(errno)); + break; + } + + err = snd_rawmidi_poll_descriptors_revents(input, &pfds[1], npfds - 1, &revents); + if (err < 0) { + error("cannot get poll events: %s", snd_strerror(errno)); + break; + } + if (revents & (POLLERR | POLLHUP)) + break; + if (!(revents & POLLIN)) { + if (pfds[0].revents & POLLIN) + break; + continue; + } + + err = snd_rawmidi_read(input, buf, sizeof(buf)); + if (err == -EAGAIN) + continue; + if (err < 0) { + error("cannot read from port \"%s\": %s", port_name, snd_strerror(err)); + break; + } + length = 0; + for (i = 0; i < err; ++i) + if ((buf[i] != MIDI_CMD_COMMON_CLOCK && + buf[i] != MIDI_CMD_COMMON_SENSING) || + (buf[i] == MIDI_CMD_COMMON_CLOCK && !ignore_clock) || + (buf[i] == MIDI_CMD_COMMON_SENSING && !ignore_active_sensing)) + buf[length++] = buf[i]; + if (length == 0) + continue; + read += length; + + if (receive_file != -1) + write(receive_file, buf, length); + if (dump) { + for (i = 0; i < length; ++i) + print_byte(buf[i], &ts); + + fflush(stdout); + } + + if (timeout > 0) { + err = timerfd_settime(pfds[0].fd, 0, &itimerspec, NULL); + if (err < 0) { + error("cannot set timer: %s", strerror(errno)); + break; + } + } + } + if (isatty(fileno(stdout))) + printf("\n%d bytes read\n", read); + } + + ok = 1; +_exit: + if (inputp) + snd_rawmidi_close(input); + if (outputp) + snd_rawmidi_close(output); +_exit2: + if (receive_file != -1) + close(receive_file); + return !ok; +} diff --git a/amixer/Makefile.am b/amixer/Makefile.am new file mode 100644 index 0000000..46ec736 --- /dev/null +++ b/amixer/Makefile.am @@ -0,0 +1,11 @@ +AM_CFLAGS = -D_GNU_SOURCE +AM_CPPFLAGS = -I$(top_srcdir)/include +LDADD = -lm +# LDFLAGS = -static +# CFLAGS += -g -Wall + +bin_PROGRAMS = amixer +amixer_SOURCES = amixer.c volume_mapping.c +noinst_HEADERS = amixer.h volume_mapping.h +man_MANS = amixer.1 +EXTRA_DIST = amixer.1 diff --git a/amixer/amixer.1 b/amixer/amixer.1 new file mode 100644 index 0000000..8860ae1 --- /dev/null +++ b/amixer/amixer.1 @@ -0,0 +1,169 @@ +.TH AMIXER 1 "11 Aug 2000" +.SH NAME +amixer \- command-line mixer for ALSA soundcard driver +.SH SYNOPSIS +\fBamixer\fP [\fI\-option\fP] [\fIcmd\fP] +.SH DESCRIPTION +\fBamixer\fP allows command\-line control of the mixer for the ALSA +soundcard driver. +\fBamixer\fP supports multiple soundcards. + +\fBamixer\fR with no arguments will display the current mixer settings +for the default soundcard and device. This is a good way to see a list +of the simple mixer controls you can use. + +.SH COMMANDS + +.TP +\fIhelp\fP +Shows syntax. + +.TP +\fIinfo\fP +Shows the information about a mixer device. + +.TP +\fIscontrols\fP +Shows a complete list of simple mixer controls. + +.TP +\fIscontents\fP +Shows a complete list of simple mixer controls with their contents. + +.TP +\fIset\fP or \fIsset\fP <\fISCONTROL\fP> <\fIPARAMETER\fP> ... +Sets the simple mixer control contents. The parameter can be the volume +either as a percentage from 0% to 100% with \fI%\fP suffix, +a dB gain with \fIdB\fP suffix (like \-12.5dB), or an exact hardware value. +The dB gain can be used only for the mixer elements with available +dB information. +When plus(+) or minus(\-) letter is appended after +volume value, the volume is incremented or decremented from the current +value, respectively. + +The parameters \fIcap, nocap, mute, unmute, toggle\fP are used to +change capture (recording) and muting for the group specified. + +The optional modifiers can be put as extra parameters before the value to +specify the stream direction or channels to apply. +The modifiers \fIplayback\fP and \fIcapture\fP specify the stream, +and the modifiers \fIfront, frontleft, frontright, frontcenter, center, +rear, rearright, rearleft, woofer\fP are used to specify channels to be +changed. + +A simple mixer control must be specified. Only one device can be controlled +at a time. + +.TP +\fIget\fP or \fIsget\fP <\fISCONTROL\fP> +Shows the simple mixer control contents. + +A simple mixer control must be specified. Only one device can be controlled +at a time. + +.TP +\fIcontrols\fP +Shows a complete list of card controls. + +.TP +\fIcontents\fP +Shows a complete list of card controls with their contents. + +.TP +\fIcset\fP <\fICONTROL\fP> <\fIPARAMETER\fP> ... +Sets the card control contents. The identifier has these components: iface, +name, index, device, subdevice, numid. The next argument specifies the value +of control. + +.TP +\fIcget\fP <\fICONTROL\fP> +Shows the card control contents. The identifier has same syntax as for +the \fIcset\fP command. + +.SH ADVANCED COMMANDS + +.TP +\fIsevents\fP +Shows the events for the simple mixer controls. + +.TP +\fIevents\fP +Shows the events for the mixer controls. + +.SH OPTIONS + +.TP +\fI\-c\fP card + +Select the card number to control. The device name created from this +parameter has syntax 'hw:N' where N is specified card number. + +.TP +\fI\-D\fP device + +Select the device name to control. The default control name is 'default'. + +.TP +\fI\-s\fP | \fI\-\-stdin\fP + +Read from stdin and execute the command on each line sequentially. +When this option is given, the command in command\-line arguments is ignored. + +Only sset and cset are accepted. Other commands are ignored. +The commands to unmatched ids are ignored without errors too. + +.TP +\fI\-h\fP +Help: show syntax. + +.TP +\fI\-q\fP +Quiet mode. Do not show results of changes. + +.TP +\fI\-R\fP +Use the raw value for evaluating the percentage representation. +This is the default mode. + +.TP +\fI\-M\fP +Use the mapped volume for evaluating the percentage representation +like \fBalsamixer\fR, to be more natural for human ear. + +.SH EXAMPLES + +.TP +\fBamixer \-c 1 sset Line,0 80%,40% unmute cap\fR +will set the second soundcard's left line input volume to 80% and +right line input to 40%, unmute it, and select it as a source for +capture (recording).\fR + +.TP +\fBamixer \-c 1 \-\- sset Master playback \-20dB\fR +will set the master volume of the second card to \-20dB. If the master +has multiple channels, all channels are set to the same value. + +.TP +\fBamixer \-c 1 set PCM 2dB+\fR +will increase the PCM volume of the second card with 2dB. When both +playback and capture volumes exist, this is applied to both volumes. + +.TP +\fBamixer \-c 2 cset iface=MIXER,name='Line Playback Volume",index=1 40%\fR +will set the third soundcard's second line playback volume(s) to 40% + +.TP +\fBamixer \-c 2 cset numid=34 40%\fR +will set the 34th soundcard element to 40% + +.SH SEE ALSO +\fB +alsamixer(1) +\fP + +.SH BUGS +None known. + +.SH AUTHOR +\fBamixer\fP is by Jaroslav Kysela . +This document is by Paul Winkler and Jaroslav Kysela . diff --git a/amixer/amixer.c b/amixer/amixer.c new file mode 100644 index 0000000..07e9819 --- /dev/null +++ b/amixer/amixer.c @@ -0,0 +1,1924 @@ +/* + * ALSA command line mixer utility + * Copyright (c) 1999-2000 by Jaroslav Kysela + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "amixer.h" +#include "../alsamixer/volume_mapping.h" + +#define LEVEL_BASIC (1<<0) +#define LEVEL_INACTIVE (1<<1) +#define LEVEL_ID (1<<2) + +static int quiet = 0; +static int debugflag = 0; +static int no_check = 0; +static int smixer_level = 0; +static int ignore_error = 0; +static struct snd_mixer_selem_regopt smixer_options; +static char card[64] = "default"; + +static void error(const char *fmt,...) +{ + va_list va; + + va_start(va, fmt); + fprintf(stderr, "amixer: "); + vfprintf(stderr, fmt, va); + fprintf(stderr, "\n"); + va_end(va); +} + +static int help(void) +{ + printf("Usage: amixer [command]\n"); + printf("\nAvailable options:\n"); + printf(" -h,--help this help\n"); + printf(" -c,--card N select the card\n"); + printf(" -D,--device N select the device, default '%s'\n", card); + printf(" -d,--debug debug mode\n"); + printf(" -n,--nocheck do not perform range checking\n"); + printf(" -v,--version print version of this program\n"); + printf(" -q,--quiet be quiet\n"); + printf(" -i,--inactive show also inactive controls\n"); + printf(" -a,--abstract L select abstraction level (none or basic)\n"); + printf(" -s,--stdin Read and execute commands from stdin sequentially\n"); + printf(" -R,--raw-volume Use the raw value (default)\n"); + printf(" -M,--mapped-volume Use the mapped volume\n"); + printf("\nAvailable commands:\n"); + printf(" scontrols show all mixer simple controls\n"); + printf(" scontents show contents of all mixer simple controls (default command)\n"); + printf(" sset sID P set contents for one mixer simple control\n"); + printf(" sget sID get contents for one mixer simple control\n"); + printf(" controls show all controls for given card\n"); + printf(" contents show contents of all controls for given card\n"); + printf(" cset cID P set control contents for one control\n"); + printf(" cget cID get control contents for one control\n"); + printf("\nAvailable advanced commands:\n"); + printf(" sevents show the mixer events for simple controls\n"); + printf(" events show the mixer events for controls\n"); + return 0; +} + +static int info(void) +{ + int err; + snd_ctl_t *handle; + snd_mixer_t *mhandle; + snd_ctl_card_info_t *info; + snd_ctl_elem_list_t *clist; + snd_ctl_card_info_alloca(&info); + snd_ctl_elem_list_alloca(&clist); + + if ((err = snd_ctl_open(&handle, card, 0)) < 0) { + error("Control device %s open error: %s", card, snd_strerror(err)); + return err; + } + + if ((err = snd_ctl_card_info(handle, info)) < 0) { + error("Control device %s hw info error: %s", card, snd_strerror(err)); + return err; + } + printf("Card %s '%s'/'%s'\n", card, snd_ctl_card_info_get_id(info), + snd_ctl_card_info_get_longname(info)); + printf(" Mixer name : '%s'\n", snd_ctl_card_info_get_mixername(info)); + printf(" Components : '%s'\n", snd_ctl_card_info_get_components(info)); + if ((err = snd_ctl_elem_list(handle, clist)) < 0) { + error("snd_ctl_elem_list failure: %s", snd_strerror(err)); + } else { + printf(" Controls : %i\n", snd_ctl_elem_list_get_count(clist)); + } + snd_ctl_close(handle); + if ((err = snd_mixer_open(&mhandle, 0)) < 0) { + error("Mixer open error: %s", snd_strerror(err)); + return err; + } + if (smixer_level == 0 && (err = snd_mixer_attach(mhandle, card)) < 0) { + error("Mixer attach %s error: %s", card, snd_strerror(err)); + snd_mixer_close(mhandle); + return err; + } + if ((err = snd_mixer_selem_register(mhandle, smixer_level > 0 ? &smixer_options : NULL, NULL)) < 0) { + error("Mixer register error: %s", snd_strerror(err)); + snd_mixer_close(mhandle); + return err; + } + err = snd_mixer_load(mhandle); + if (err < 0) { + error("Mixer load %s error: %s", card, snd_strerror(err)); + snd_mixer_close(mhandle); + return err; + } + printf(" Simple ctrls : %i\n", snd_mixer_get_count(mhandle)); + snd_mixer_close(mhandle); + return 0; +} + +static const char *control_type(snd_ctl_elem_info_t *info) +{ + return snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)); +} + +static const char *control_access(snd_ctl_elem_info_t *info) +{ + static char result[10]; + char *res = result; + + *res++ = snd_ctl_elem_info_is_readable(info) ? 'r' : '-'; + *res++ = snd_ctl_elem_info_is_writable(info) ? 'w' : '-'; + *res++ = snd_ctl_elem_info_is_inactive(info) ? 'i' : '-'; + *res++ = snd_ctl_elem_info_is_volatile(info) ? 'v' : '-'; + *res++ = snd_ctl_elem_info_is_locked(info) ? 'l' : '-'; + *res++ = snd_ctl_elem_info_is_tlv_readable(info) ? 'R' : '-'; + *res++ = snd_ctl_elem_info_is_tlv_writable(info) ? 'W' : '-'; + *res++ = snd_ctl_elem_info_is_tlv_commandable(info) ? 'C' : '-'; + *res++ = '\0'; + return result; +} + +#define check_range(val, min, max) \ + (no_check ? (val) : ((val < min) ? (min) : (val > max) ? (max) : (val))) +#if 0 +static int convert_range(int val, int omin, int omax, int nmin, int nmax) +{ + int orange = omax - omin, nrange = nmax - nmin; + + if (orange == 0) + return 0; + return rint((((double)nrange * ((double)val - (double)omin)) + ((double)orange / 2.0)) / ((double)orange + (double)nmin)); +} +#endif + +#if 0 +static int convert_db_range(int val, int omin, int omax, int nmin, int nmax) +{ + int orange = omax - omin, nrange = nmax - nmin; + + if (orange == 0) + return 0; + return rint((((double)nrange * ((double)val - (double)omin)) + ((double)orange / 2.0)) / (double)orange + (double)nmin); +} +#endif + +/* Fuction to convert from volume to percentage. val = volume */ + +static int convert_prange(long val, long min, long max) +{ + long range = max - min; + int tmp; + + if (range == 0) + return min; + val -= min; + tmp = rint((double)val/(double)range * 100); + return tmp; +} + +/* Function to convert from percentage to volume. perc = percentage */ +static long convert_prange1(long perc, long min, long max) +{ + long tmp; + + tmp = rint((double)perc * (double)(max - min) * 0.01); + if (tmp == 0 && perc > 0) + tmp++; + return tmp + min; +} + +struct volume_ops { + int (*get_range)(snd_mixer_elem_t *elem, long *min, long *max); + int (*get)(snd_mixer_elem_t *elem, snd_mixer_selem_channel_id_t c, + long *value); + int (*set)(snd_mixer_elem_t *elem, snd_mixer_selem_channel_id_t c, + long value, int dir); +}; + +enum { VOL_RAW, VOL_DB, VOL_MAP }; + +struct volume_ops_set { + int (*has_volume)(snd_mixer_elem_t *elem); + struct volume_ops v[3]; +}; + +static int set_playback_dB(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, long value, int dir) +{ + return snd_mixer_selem_set_playback_dB(elem, c, value, dir); +} + +static int set_capture_dB(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, long value, int dir) +{ + return snd_mixer_selem_set_capture_dB(elem, c, value, dir); +} + +static int set_playback_raw_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, + long value, int dir ATTRIBUTE_UNUSED) +{ + return snd_mixer_selem_set_playback_volume(elem, c, value); +} + +static int set_capture_raw_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, + long value, int dir ATTRIBUTE_UNUSED) +{ + return snd_mixer_selem_set_capture_volume(elem, c, value); +} + +/* FIXME: normalize to int32 space to be compatible with other types */ +#define MAP_VOL_RES (INT32_MAX / 100) + +static int get_mapped_volume_range(snd_mixer_elem_t *elem ATTRIBUTE_UNUSED, + long *pmin, long *pmax) +{ + *pmin = 0; + *pmax = MAP_VOL_RES; + return 0; +} + +static int get_playback_mapped_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, + long *value) +{ + *value = (rint)(get_normalized_playback_volume(elem, c) * MAP_VOL_RES); + return 0; +} + +static int set_playback_mapped_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, + long value, int dir) +{ + return set_normalized_playback_volume(elem, c, + (double)value / MAP_VOL_RES, dir); +} + +static int get_capture_mapped_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, + long *value) +{ + *value = (rint)(get_normalized_capture_volume(elem, c) * MAP_VOL_RES); + return 0; +} + +static int set_capture_mapped_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t c, + long value, int dir) +{ + return set_normalized_capture_volume(elem, c, + (double)value / MAP_VOL_RES, dir); +} + +static const struct volume_ops_set vol_ops[2] = { + { + .has_volume = snd_mixer_selem_has_playback_volume, + .v = {{ snd_mixer_selem_get_playback_volume_range, + snd_mixer_selem_get_playback_volume, + set_playback_raw_volume }, + { snd_mixer_selem_get_playback_dB_range, + snd_mixer_selem_get_playback_dB, + set_playback_dB }, + { get_mapped_volume_range, + get_playback_mapped_volume, + set_playback_mapped_volume }, + }, + }, + { + .has_volume = snd_mixer_selem_has_capture_volume, + .v = {{ snd_mixer_selem_get_capture_volume_range, + snd_mixer_selem_get_capture_volume, + set_capture_raw_volume }, + { snd_mixer_selem_get_capture_dB_range, + snd_mixer_selem_get_capture_dB, + set_capture_dB }, + { get_mapped_volume_range, + get_capture_mapped_volume, + set_capture_mapped_volume }, + }, + }, +}; + +static int std_vol_type = VOL_RAW; + +static int set_volume_simple(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t chn, + char **ptr, int dir) +{ + long val, orig, pmin, pmax; + char *p = *ptr, *s; + int invalid = 0, percent = 0, err = 0; + int vol_type; + double scale = 1.0; + int correct = 0; + + if (! vol_ops[dir].has_volume(elem)) + invalid = 1; + + if (*p == ':') + p++; + if (*p == '\0' || (!isdigit(*p) && *p != '-')) + goto skip; + + s = p; + val = strtol(s, &p, 10); + if (*p == '.') { + p++; + strtol(p, &p, 10); + } + if (*p == '%') { + vol_type = std_vol_type; + percent = 1; + p++; + } else if (toupper(p[0]) == 'D' && toupper(p[1]) == 'B') { + vol_type = VOL_DB; + p += 2; + scale = 100; + } else { + vol_type = VOL_RAW; + } + + if (*p && !strchr(",:+-", *p)) + invalid = 1; + + val = (long)(strtod(s, NULL) * scale); + if (vol_ops[dir].v[vol_type].get_range(elem, &pmin, &pmax) < 0) + invalid = 1; + if (percent) + val = (long)convert_prange1(val, pmin, pmax); + if (*p == '+' || *p == '-') { + if (! invalid) { + if (vol_ops[dir].v[vol_type].get(elem, chn, &orig) < 0) + invalid = 1; + if (*p == '+') { + val = orig + val; + correct = 1; + } else { + val = orig - val; + correct = -1; + } + } + p++; + } + + if (*p && !strchr(",:", *p)) + invalid = 1; + + if (! invalid) { + val = check_range(val, pmin, pmax); + err = vol_ops[dir].v[vol_type].set(elem, chn, val, correct); + } + skip: + if (*p == ',') + p++; + *ptr = p; + return err ? err : (invalid ? -ENOENT : 0); +} + +static int get_bool_simple(char **ptr, char *str, int invert, int orig) +{ + if (**ptr == ':') + (*ptr)++; + if (!strncasecmp(*ptr, str, strlen(str))) { + orig = 1 ^ (invert ? 1 : 0); + while (**ptr != '\0' && **ptr != ',' && **ptr != ':') + (*ptr)++; + } + if (**ptr == ',' || **ptr == ':') + (*ptr)++; + return orig; +} + +static int simple_skip_word(char **ptr, char *str) +{ + char *xptr = *ptr; + if (*xptr == ':') + xptr++; + if (!strncasecmp(xptr, str, strlen(str))) { + while (*xptr != '\0' && *xptr != ',' && *xptr != ':') + xptr++; + if (*xptr == ',' || *xptr == ':') + xptr++; + *ptr = xptr; + return 1; + } + return 0; +} + +static void show_control_id(snd_ctl_elem_id_t *id) +{ + char *str; + + str = snd_ctl_ascii_elem_id_get(id); + if (str) + printf("%s", str); + free(str); +} + +static void print_spaces(unsigned int spaces) +{ + while (spaces-- > 0) + putc(' ', stdout); +} + +static void print_dB(long dB) +{ + if (dB < 0) { + printf("-%li.%02lidB", -dB / 100, -dB % 100); + } else { + printf("%li.%02lidB", dB / 100, dB % 100); + } +} + +static void decode_tlv(unsigned int spaces, unsigned int *tlv, unsigned int tlv_size) +{ + unsigned int type = tlv[0]; + unsigned int size; + unsigned int idx = 0; + const char *chmap_type = NULL; + int lf = 1; + + if (tlv_size < 2 * sizeof(unsigned int)) { + printf("TLV size error!\n"); + return; + } + print_spaces(spaces); + printf("| "); + type = tlv[idx++]; + size = tlv[idx++]; + tlv_size -= 2 * sizeof(unsigned int); + if (size > tlv_size) { + printf("TLV size error (%u, %u, %u)!\n", type, size, tlv_size); + return; + } + switch (type) { + case SND_CTL_TLVT_CONTAINER: + printf("container\n"); + size += sizeof(unsigned int) -1; + size /= sizeof(unsigned int); + while (idx < size) { + if (tlv[idx+1] > (size - idx) * sizeof(unsigned int)) { + printf("TLV size error in compound!\n"); + return; + } + decode_tlv(spaces + 2, tlv + idx, tlv[idx+1] + 8); + idx += 2 + (tlv[idx+1] + sizeof(unsigned int) - 1) / sizeof(unsigned int); + } + lf = 0; + break; + case SND_CTL_TLVT_DB_SCALE: + printf("dBscale-"); + if (size != 2 * sizeof(unsigned int)) { + while (size > 0) { + printf("0x%08x,", tlv[idx++]); + size -= sizeof(unsigned int); + } + } else { + printf("min="); + print_dB((int)tlv[2]); + printf(",step="); + print_dB(tlv[3] & 0xffff); + printf(",mute=%i", (tlv[3] >> 16) & 1); + } + break; +#ifdef SND_CTL_TLVT_DB_LINEAR + case SND_CTL_TLVT_DB_LINEAR: + printf("dBlinear-"); + if (size != 2 * sizeof(unsigned int)) { + while (size > 0) { + printf("0x%08x,", tlv[idx++]); + size -= sizeof(unsigned int); + } + } else { + printf("min="); + print_dB((int)tlv[2]); + printf(",max="); + print_dB((int)tlv[3]); + } + break; +#endif +#ifdef SND_CTL_TLVT_DB_RANGE + case SND_CTL_TLVT_DB_RANGE: + printf("dBrange-\n"); + if ((size % (6 * sizeof(unsigned int))) != 0) { + while (size > 0) { + printf("0x%08x,", tlv[idx++]); + size -= sizeof(unsigned int); + } + break; + } + while (size > 0) { + print_spaces(spaces + 2); + printf("rangemin=%i,", tlv[idx++]); + printf(",rangemax=%i\n", tlv[idx++]); + decode_tlv(spaces + 4, tlv + idx, 4 * sizeof(unsigned int)); + idx += 4; + size -= 6 * sizeof(unsigned int); + } + break; +#endif +#ifdef SND_CTL_TLVT_DB_MINMAX + case SND_CTL_TLVT_DB_MINMAX: + case SND_CTL_TLVT_DB_MINMAX_MUTE: + if (type == SND_CTL_TLVT_DB_MINMAX_MUTE) + printf("dBminmaxmute-"); + else + printf("dBminmax-"); + if (size != 2 * sizeof(unsigned int)) { + while (size > 0) { + printf("0x%08x,", tlv[idx++]); + size -= sizeof(unsigned int); + } + } else { + printf("min="); + print_dB((int)tlv[2]); + printf(",max="); + print_dB((int)tlv[3]); + } + break; +#endif +#ifdef SND_CTL_TLVT_CHMAP_FIXED + case SND_CTL_TLVT_CHMAP_FIXED: + chmap_type = "fixed"; + /* Fall through */ + case SND_CTL_TLVT_CHMAP_VAR: + if (!chmap_type) + chmap_type = "variable"; + /* Fall through */ + case SND_CTL_TLVT_CHMAP_PAIRED: + if (!chmap_type) + chmap_type = "paired"; + printf("chmap-%s=", chmap_type); + + while (size > 0) { + printf("%s", snd_pcm_chmap_name(tlv[idx++])); + size -= sizeof(unsigned int); + if (size > 0) + printf(","); + } + break; +#endif + default: + printf("unk-%u-", type); + while (size > sizeof(unsigned int)) { + printf("0x%08x,", tlv[idx++]); + size -= sizeof(unsigned int); + } + if (size > 0) { + unsigned char *b = (void *)&tlv[idx]; + while (size > 0) { + printf("E-0x%02x,", *b++); + size--; + } + } + break; + } + if (lf) + putc('\n', stdout); +} + +static int show_control(const char *space, snd_hctl_elem_t *elem, + int level) +{ + int err; + unsigned int item, idx, count, *tlv; + snd_ctl_elem_type_t type; + snd_ctl_elem_id_t *id; + snd_ctl_elem_info_t *info; + snd_ctl_elem_value_t *control; + snd_aes_iec958_t iec958; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_value_alloca(&control); + if ((err = snd_hctl_elem_info(elem, info)) < 0) { + error("Control %s snd_hctl_elem_info error: %s\n", card, snd_strerror(err)); + return err; + } + if (level & LEVEL_ID) { + snd_hctl_elem_get_id(elem, id); + show_control_id(id); + printf("\n"); + } + count = snd_ctl_elem_info_get_count(info); + type = snd_ctl_elem_info_get_type(info); + printf("%s; type=%s,access=%s,values=%u", space, control_type(info), control_access(info), count); + switch (type) { + case SND_CTL_ELEM_TYPE_INTEGER: + printf(",min=%li,max=%li,step=%li\n", + snd_ctl_elem_info_get_min(info), + snd_ctl_elem_info_get_max(info), + snd_ctl_elem_info_get_step(info)); + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + printf(",min=%lli,max=%lli,step=%lli\n", + snd_ctl_elem_info_get_min64(info), + snd_ctl_elem_info_get_max64(info), + snd_ctl_elem_info_get_step64(info)); + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + { + unsigned int items = snd_ctl_elem_info_get_items(info); + printf(",items=%u\n", items); + for (item = 0; item < items; item++) { + snd_ctl_elem_info_set_item(info, item); + if ((err = snd_hctl_elem_info(elem, info)) < 0) { + error("Control %s element info error: %s\n", card, snd_strerror(err)); + return err; + } + printf("%s; Item #%u '%s'\n", space, item, snd_ctl_elem_info_get_item_name(info)); + } + break; + } + default: + printf("\n"); + break; + } + if (level & LEVEL_BASIC) { + if (!snd_ctl_elem_info_is_readable(info)) + goto __skip_read; + if ((err = snd_hctl_elem_read(elem, control)) < 0) { + error("Control %s element read error: %s\n", card, snd_strerror(err)); + return err; + } + printf("%s: values=", space); + for (idx = 0; idx < count; idx++) { + if (idx > 0) + printf(","); + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + printf("%s", snd_ctl_elem_value_get_boolean(control, idx) ? "on" : "off"); + break; + case SND_CTL_ELEM_TYPE_INTEGER: + printf("%li", snd_ctl_elem_value_get_integer(control, idx)); + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + printf("%lli", snd_ctl_elem_value_get_integer64(control, idx)); + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + printf("%u", snd_ctl_elem_value_get_enumerated(control, idx)); + break; + case SND_CTL_ELEM_TYPE_BYTES: + printf("0x%02x", snd_ctl_elem_value_get_byte(control, idx)); + break; + case SND_CTL_ELEM_TYPE_IEC958: + snd_ctl_elem_value_get_iec958(control, &iec958); + printf("[AES0=0x%02x AES1=0x%02x AES2=0x%02x AES3=0x%02x]", + iec958.status[0], iec958.status[1], + iec958.status[2], iec958.status[3]); + break; + default: + printf("?"); + break; + } + } + printf("\n"); + __skip_read: + if (!snd_ctl_elem_info_is_tlv_readable(info)) + goto __skip_tlv; + /* skip ASoC ext bytes controls that may have huge binary TLV data */ + if (type == SND_CTL_ELEM_TYPE_BYTES && + !snd_ctl_elem_info_is_readable(info) && + !snd_ctl_elem_info_is_writable(info)) { + printf("%s; ASoC TLV Byte control, skipping bytes dump\n", space); + goto __skip_tlv; + } + + tlv = malloc(4096); + if ((err = snd_hctl_elem_tlv_read(elem, tlv, 4096)) < 0) { + error("Control %s element TLV read error: %s\n", card, snd_strerror(err)); + free(tlv); + return err; + } + decode_tlv(strlen(space), tlv, 4096); + free(tlv); + } + __skip_tlv: + return 0; +} + +static int controls(int level) +{ + int err; + snd_hctl_t *handle; + snd_hctl_elem_t *elem; + snd_ctl_elem_id_t *id; + snd_ctl_elem_info_t *info; + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_info_alloca(&info); + + if ((err = snd_hctl_open(&handle, card, 0)) < 0) { + error("Control %s open error: %s", card, snd_strerror(err)); + return err; + } + if ((err = snd_hctl_load(handle)) < 0) { + error("Control %s local error: %s\n", card, snd_strerror(err)); + return err; + } + for (elem = snd_hctl_first_elem(handle); elem; elem = snd_hctl_elem_next(elem)) { + if ((err = snd_hctl_elem_info(elem, info)) < 0) { + error("Control %s snd_hctl_elem_info error: %s\n", card, snd_strerror(err)); + return err; + } + if (!(level & LEVEL_INACTIVE) && snd_ctl_elem_info_is_inactive(info)) + continue; + snd_hctl_elem_get_id(elem, id); + show_control_id(id); + printf("\n"); + if (level & LEVEL_BASIC) + show_control(" ", elem, 1); + } + snd_hctl_close(handle); + return 0; +} + +static void show_selem_volume(snd_mixer_elem_t *elem, + snd_mixer_selem_channel_id_t chn, int dir, + long min, long max) +{ + long raw, val; + vol_ops[dir].v[VOL_RAW].get(elem, chn, &raw); + if (std_vol_type == VOL_RAW) + val = convert_prange(raw, min, max); + else { + vol_ops[dir].v[std_vol_type].get(elem, chn, &val); + val = convert_prange(val, 0, MAP_VOL_RES); + } + printf(" %li [%li%%]", raw, val); + if (!vol_ops[dir].v[VOL_DB].get(elem, chn, &val)) { + printf(" ["); + print_dB(val); + printf("]"); + } +} + +static int show_selem(snd_mixer_t *handle, snd_mixer_selem_id_t *id, const char *space, int level) +{ + snd_mixer_selem_channel_id_t chn; + long pmin = 0, pmax = 0; + long cmin = 0, cmax = 0; + int psw, csw; + int pmono, cmono, mono_ok = 0; + snd_mixer_elem_t *elem; + + elem = snd_mixer_find_selem(handle, id); + if (!elem) { + error("Mixer %s simple element not found", card); + return -ENOENT; + } + + if (level & LEVEL_BASIC) { + printf("%sCapabilities:", space); + if (snd_mixer_selem_has_common_volume(elem)) { + printf(" volume"); + if (snd_mixer_selem_has_playback_volume_joined(elem)) + printf(" volume-joined"); + } else { + if (snd_mixer_selem_has_playback_volume(elem)) { + printf(" pvolume"); + if (snd_mixer_selem_has_playback_volume_joined(elem)) + printf(" pvolume-joined"); + } + if (snd_mixer_selem_has_capture_volume(elem)) { + printf(" cvolume"); + if (snd_mixer_selem_has_capture_volume_joined(elem)) + printf(" cvolume-joined"); + } + } + if (snd_mixer_selem_has_common_switch(elem)) { + printf(" switch"); + if (snd_mixer_selem_has_playback_switch_joined(elem)) + printf(" switch-joined"); + } else { + if (snd_mixer_selem_has_playback_switch(elem)) { + printf(" pswitch"); + if (snd_mixer_selem_has_playback_switch_joined(elem)) + printf(" pswitch-joined"); + } + if (snd_mixer_selem_has_capture_switch(elem)) { + printf(" cswitch"); + if (snd_mixer_selem_has_capture_switch_joined(elem)) + printf(" cswitch-joined"); + if (snd_mixer_selem_has_capture_switch_exclusive(elem)) + printf(" cswitch-exclusive"); + } + } + if (snd_mixer_selem_is_enum_playback(elem)) { + printf(" penum"); + } else if (snd_mixer_selem_is_enum_capture(elem)) { + printf(" cenum"); + } else if (snd_mixer_selem_is_enumerated(elem)) { + printf(" enum"); + } + printf("\n"); + if (snd_mixer_selem_is_enumerated(elem)) { + int i, items; + unsigned int idx; + /* + * See snd_ctl_elem_init_enum_names() in + * sound/core/control.c. + */ + char itemname[64]; + items = snd_mixer_selem_get_enum_items(elem); + printf(" Items:"); + for (i = 0; i < items; i++) { + snd_mixer_selem_get_enum_item_name(elem, i, sizeof(itemname) - 1, itemname); + printf(" '%s'", itemname); + } + printf("\n"); + for (i = 0; !snd_mixer_selem_get_enum_item(elem, i, &idx); i++) { + snd_mixer_selem_get_enum_item_name(elem, idx, sizeof(itemname) - 1, itemname); + printf(" Item%d: '%s'\n", i, itemname); + } + return 0; /* no more thing to do */ + } + if (snd_mixer_selem_has_capture_switch_exclusive(elem)) + printf("%sCapture exclusive group: %i\n", space, + snd_mixer_selem_get_capture_group(elem)); + if (snd_mixer_selem_has_playback_volume(elem) || + snd_mixer_selem_has_playback_switch(elem)) { + printf("%sPlayback channels:", space); + if (snd_mixer_selem_is_playback_mono(elem)) { + printf(" Mono"); + } else { + int first = 1; + for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++){ + if (!snd_mixer_selem_has_playback_channel(elem, chn)) + continue; + if (!first) + printf(" -"); + printf(" %s", snd_mixer_selem_channel_name(chn)); + first = 0; + } + } + printf("\n"); + } + if (snd_mixer_selem_has_capture_volume(elem) || + snd_mixer_selem_has_capture_switch(elem)) { + printf("%sCapture channels:", space); + if (snd_mixer_selem_is_capture_mono(elem)) { + printf(" Mono"); + } else { + int first = 1; + for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++){ + if (!snd_mixer_selem_has_capture_channel(elem, chn)) + continue; + if (!first) + printf(" -"); + printf(" %s", snd_mixer_selem_channel_name(chn)); + first = 0; + } + } + printf("\n"); + } + if (snd_mixer_selem_has_playback_volume(elem) || + snd_mixer_selem_has_capture_volume(elem)) { + printf("%sLimits:", space); + if (snd_mixer_selem_has_common_volume(elem)) { + snd_mixer_selem_get_playback_volume_range(elem, &pmin, &pmax); + snd_mixer_selem_get_capture_volume_range(elem, &cmin, &cmax); + printf(" %li - %li", pmin, pmax); + } else { + if (snd_mixer_selem_has_playback_volume(elem)) { + snd_mixer_selem_get_playback_volume_range(elem, &pmin, &pmax); + printf(" Playback %li - %li", pmin, pmax); + } + if (snd_mixer_selem_has_capture_volume(elem)) { + snd_mixer_selem_get_capture_volume_range(elem, &cmin, &cmax); + printf(" Capture %li - %li", cmin, cmax); + } + } + printf("\n"); + } + pmono = snd_mixer_selem_has_playback_channel(elem, SND_MIXER_SCHN_MONO) && + (snd_mixer_selem_is_playback_mono(elem) || + (!snd_mixer_selem_has_playback_volume(elem) && + !snd_mixer_selem_has_playback_switch(elem))); + cmono = snd_mixer_selem_has_capture_channel(elem, SND_MIXER_SCHN_MONO) && + (snd_mixer_selem_is_capture_mono(elem) || + (!snd_mixer_selem_has_capture_volume(elem) && + !snd_mixer_selem_has_capture_switch(elem))); +#if 0 + printf("pmono = %i, cmono = %i (%i, %i, %i, %i)\n", pmono, cmono, + snd_mixer_selem_has_capture_channel(elem, SND_MIXER_SCHN_MONO), + snd_mixer_selem_is_capture_mono(elem), + snd_mixer_selem_has_capture_volume(elem), + snd_mixer_selem_has_capture_switch(elem)); +#endif + if (pmono || cmono) { + if (!mono_ok) { + printf("%s%s:", space, "Mono"); + mono_ok = 1; + } + if (snd_mixer_selem_has_common_volume(elem)) { + show_selem_volume(elem, SND_MIXER_SCHN_MONO, 0, pmin, pmax); + } + if (snd_mixer_selem_has_common_switch(elem)) { + snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_MONO, &psw); + printf(" [%s]", psw ? "on" : "off"); + } + } + if (pmono && snd_mixer_selem_has_playback_channel(elem, SND_MIXER_SCHN_MONO)) { + int title = 0; + if (!mono_ok) { + printf("%s%s:", space, "Mono"); + mono_ok = 1; + } + if (!snd_mixer_selem_has_common_volume(elem)) { + if (snd_mixer_selem_has_playback_volume(elem)) { + printf(" Playback"); + title = 1; + show_selem_volume(elem, SND_MIXER_SCHN_MONO, 0, pmin, pmax); + } + } + if (!snd_mixer_selem_has_common_switch(elem)) { + if (snd_mixer_selem_has_playback_switch(elem)) { + if (!title) + printf(" Playback"); + snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_MONO, &psw); + printf(" [%s]", psw ? "on" : "off"); + } + } + } + if (cmono && snd_mixer_selem_has_capture_channel(elem, SND_MIXER_SCHN_MONO)) { + int title = 0; + if (!mono_ok) { + printf("%s%s:", space, "Mono"); + mono_ok = 1; + } + if (!snd_mixer_selem_has_common_volume(elem)) { + if (snd_mixer_selem_has_capture_volume(elem)) { + printf(" Capture"); + title = 1; + show_selem_volume(elem, SND_MIXER_SCHN_MONO, 1, cmin, cmax); + } + } + if (!snd_mixer_selem_has_common_switch(elem)) { + if (snd_mixer_selem_has_capture_switch(elem)) { + if (!title) + printf(" Capture"); + snd_mixer_selem_get_capture_switch(elem, SND_MIXER_SCHN_MONO, &csw); + printf(" [%s]", csw ? "on" : "off"); + } + } + } + if (pmono || cmono) + printf("\n"); + if (!pmono || !cmono) { + for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) { + if ((pmono || !snd_mixer_selem_has_playback_channel(elem, chn)) && + (cmono || !snd_mixer_selem_has_capture_channel(elem, chn))) + continue; + printf("%s%s:", space, snd_mixer_selem_channel_name(chn)); + if (!pmono && !cmono && snd_mixer_selem_has_common_volume(elem)) { + show_selem_volume(elem, chn, 0, pmin, pmax); + } + if (!pmono && !cmono && snd_mixer_selem_has_common_switch(elem)) { + snd_mixer_selem_get_playback_switch(elem, chn, &psw); + printf(" [%s]", psw ? "on" : "off"); + } + if (!pmono && snd_mixer_selem_has_playback_channel(elem, chn)) { + int title = 0; + if (!snd_mixer_selem_has_common_volume(elem)) { + if (snd_mixer_selem_has_playback_volume(elem)) { + printf(" Playback"); + title = 1; + show_selem_volume(elem, chn, 0, pmin, pmax); + } + } + if (!snd_mixer_selem_has_common_switch(elem)) { + if (snd_mixer_selem_has_playback_switch(elem)) { + if (!title) + printf(" Playback"); + snd_mixer_selem_get_playback_switch(elem, chn, &psw); + printf(" [%s]", psw ? "on" : "off"); + } + } + } + if (!cmono && snd_mixer_selem_has_capture_channel(elem, chn)) { + int title = 0; + if (!snd_mixer_selem_has_common_volume(elem)) { + if (snd_mixer_selem_has_capture_volume(elem)) { + printf(" Capture"); + title = 1; + show_selem_volume(elem, chn, 1, cmin, cmax); + } + } + if (!snd_mixer_selem_has_common_switch(elem)) { + if (snd_mixer_selem_has_capture_switch(elem)) { + if (!title) + printf(" Capture"); + snd_mixer_selem_get_capture_switch(elem, chn, &csw); + printf(" [%s]", csw ? "on" : "off"); + } + } + } + printf("\n"); + } + } + } + return 0; +} + +static int selems(int level) +{ + int err; + snd_mixer_t *handle; + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t *elem; + snd_mixer_selem_id_alloca(&sid); + + if ((err = snd_mixer_open(&handle, 0)) < 0) { + error("Mixer %s open error: %s", card, snd_strerror(err)); + return err; + } + if (smixer_level == 0 && (err = snd_mixer_attach(handle, card)) < 0) { + error("Mixer attach %s error: %s", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + if ((err = snd_mixer_selem_register(handle, smixer_level > 0 ? &smixer_options : NULL, NULL)) < 0) { + error("Mixer register error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + err = snd_mixer_load(handle); + if (err < 0) { + error("Mixer %s load error: %s", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + for (elem = snd_mixer_first_elem(handle); elem; elem = snd_mixer_elem_next(elem)) { + snd_mixer_selem_get_id(elem, sid); + if (!(level & LEVEL_INACTIVE) && !snd_mixer_selem_is_active(elem)) + continue; + printf("Simple mixer control '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); + show_selem(handle, sid, " ", level); + } + snd_mixer_close(handle); + return 0; +} + +static int parse_simple_id(const char *str, snd_mixer_selem_id_t *sid) +{ + int c, size; + char buf[128]; + char *ptr = buf; + + while (*str == ' ' || *str == '\t') + str++; + if (!(*str)) + return -EINVAL; + size = 1; /* for '\0' */ + if (*str != '"' && *str != '\'') { + while (*str && *str != ',') { + if (size < (int)sizeof(buf)) { + *ptr++ = *str; + size++; + } + str++; + } + } else { + c = *str++; + while (*str && *str != c) { + if (size < (int)sizeof(buf)) { + *ptr++ = *str; + size++; + } + str++; + } + if (*str == c) + str++; + } + if (*str == '\0') { + snd_mixer_selem_id_set_index(sid, 0); + *ptr = 0; + goto _set; + } + if (*str != ',') + return -EINVAL; + *ptr = 0; /* terminate the string */ + str++; + if (!isdigit(*str)) + return -EINVAL; + snd_mixer_selem_id_set_index(sid, atoi(str)); + _set: + snd_mixer_selem_id_set_name(sid, buf); + return 0; +} + +static int cset(int argc, char *argv[], int roflag, int keep_handle) +{ + int err; + static snd_ctl_t *handle = NULL; + snd_ctl_elem_info_t *info; + snd_ctl_elem_id_t *id; + snd_ctl_elem_value_t *control; + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&control); + + if (argc < 1) { + fprintf(stderr, "Specify a full control identifier: [[iface=,][name='name',][index=,][device=,][subdevice=]]|[numid=]\n"); + return -EINVAL; + } + if (snd_ctl_ascii_elem_id_parse(id, argv[0])) { + fprintf(stderr, "Wrong control identifier: %s\n", argv[0]); + return -EINVAL; + } + if (debugflag) { + printf("VERIFY ID: "); + show_control_id(id); + printf("\n"); + } + if (handle == NULL && + (err = snd_ctl_open(&handle, card, 0)) < 0) { + error("Control %s open error: %s\n", card, snd_strerror(err)); + return err; + } + snd_ctl_elem_info_set_id(info, id); + if ((err = snd_ctl_elem_info(handle, info)) < 0) { + if (ignore_error) + return 0; + error("Cannot find the given element from control %s\n", card); + if (! keep_handle) { + snd_ctl_close(handle); + handle = NULL; + } + return err; + } + snd_ctl_elem_info_get_id(info, id); /* FIXME: Remove it when hctl find works ok !!! */ + if (!roflag) { + snd_ctl_elem_value_set_id(control, id); + if ((err = snd_ctl_elem_read(handle, control)) < 0) { + if (ignore_error) + return 0; + error("Cannot read the given element from control %s\n", card); + if (! keep_handle) { + snd_ctl_close(handle); + handle = NULL; + } + return err; + } + err = snd_ctl_ascii_value_parse(handle, control, info, argv[1]); + if (err < 0) { + if (!ignore_error) + error("Control %s parse error: %s\n", card, snd_strerror(err)); + if (!keep_handle) { + snd_ctl_close(handle); + handle = NULL; + } + return ignore_error ? 0 : err; + } + if ((err = snd_ctl_elem_write(handle, control)) < 0) { + if (!ignore_error) + error("Control %s element write error: %s\n", card, snd_strerror(err)); + if (!keep_handle) { + snd_ctl_close(handle); + handle = NULL; + } + return ignore_error ? 0 : err; + } + } + if (! keep_handle) { + snd_ctl_close(handle); + handle = NULL; + } + if (!quiet) { + snd_hctl_t *hctl; + snd_hctl_elem_t *elem; + if ((err = snd_hctl_open(&hctl, card, 0)) < 0) { + error("Control %s open error: %s\n", card, snd_strerror(err)); + return err; + } + if ((err = snd_hctl_load(hctl)) < 0) { + error("Control %s load error: %s\n", card, snd_strerror(err)); + return err; + } + elem = snd_hctl_find_elem(hctl, id); + if (elem) + show_control(" ", elem, LEVEL_BASIC | LEVEL_ID); + else + printf("Could not find the specified element\n"); + snd_hctl_close(hctl); + } + return 0; +} + +typedef struct channel_mask { + char *name; + unsigned int mask; +} channel_mask_t; +static const channel_mask_t chanmask[] = { + {"frontleft", 1 << SND_MIXER_SCHN_FRONT_LEFT}, + {"frontright", 1 << SND_MIXER_SCHN_FRONT_RIGHT}, + {"frontcenter", 1 << SND_MIXER_SCHN_FRONT_CENTER}, + {"front", ((1 << SND_MIXER_SCHN_FRONT_LEFT) | + (1 << SND_MIXER_SCHN_FRONT_RIGHT))}, + {"center", 1 << SND_MIXER_SCHN_FRONT_CENTER}, + {"rearleft", 1 << SND_MIXER_SCHN_REAR_LEFT}, + {"rearright", 1 << SND_MIXER_SCHN_REAR_RIGHT}, + {"rear", ((1 << SND_MIXER_SCHN_REAR_LEFT) | + (1 << SND_MIXER_SCHN_REAR_RIGHT))}, + {"woofer", 1 << SND_MIXER_SCHN_WOOFER}, + {NULL, 0} +}; + +static unsigned int channels_mask(char **arg, unsigned int def) +{ + const channel_mask_t *c; + + for (c = chanmask; c->name; c++) { + if (strncasecmp(*arg, c->name, strlen(c->name)) == 0) { + while (**arg != '\0' && **arg != ',' && **arg != ' ' && **arg != '\t') + (*arg)++; + if (**arg == ',' || **arg == ' ' || **arg == '\t') + (*arg)++; + return c->mask; + } + } + return def; +} + +static unsigned int dir_mask(char **arg, unsigned int def) +{ + int findend = 0; + + if (strncasecmp(*arg, "playback", 8) == 0) + def = findend = 1; + else if (strncasecmp(*arg, "capture", 8) == 0) + def = findend = 2; + if (findend) { + while (**arg != '\0' && **arg != ',' && **arg != ' ' && **arg != '\t') + (*arg)++; + if (**arg == ',' || **arg == ' ' || **arg == '\t') + (*arg)++; + } + return def; +} + +static int get_enum_item_index(snd_mixer_elem_t *elem, char **ptrp) +{ + char *ptr = *ptrp; + int items, i, len; + + /* See snd_ctl_elem_init_enum_names() in sound/core/control.c. */ + char name[64]; + + items = snd_mixer_selem_get_enum_items(elem); + if (items <= 0) + return -1; + + for (i = 0; i < items; i++) { + if (snd_mixer_selem_get_enum_item_name(elem, i, sizeof(name)-1, name) < 0) + continue; + + len = strlen(name); + if (! strncmp(name, ptr, len)) { + if (! ptr[len] || ptr[len] == ',' || ptr[len] == '\n') { + ptr += len; + *ptrp = ptr; + return i; + } + } + } + return -1; +} + +static int sset_enum(snd_mixer_elem_t *elem, unsigned int argc, char **argv) +{ + unsigned int idx, item = 0; + int check_flag = ignore_error ? 0 : -1; + + for (idx = 1; idx < argc; idx++) { + char *ptr = argv[idx]; + while (*ptr) { + int ival = get_enum_item_index(elem, &ptr); + if (ival < 0) + return check_flag; + if (snd_mixer_selem_set_enum_item(elem, item++, ival) >= 0) + check_flag = 1; + /* skip separators */ + while (*ptr == ',' || isspace(*ptr)) + ptr++; + } + } + return check_flag; +} + +static int sset_channels(snd_mixer_elem_t *elem, unsigned int argc, char **argv) +{ + unsigned int channels = ~0U; + unsigned int dir = 3, okflag = 3; + unsigned int idx; + snd_mixer_selem_channel_id_t chn; + int check_flag = ignore_error ? 0 : -1; + + for (idx = 1; idx < argc; idx++) { + char *ptr = argv[idx], *optr; + int multi, firstchn = 1; + channels = channels_mask(&ptr, channels); + if (*ptr == '\0') + continue; + dir = dir_mask(&ptr, dir); + if (*ptr == '\0') + continue; + multi = (strchr(ptr, ',') != NULL); + optr = ptr; + for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) { + char *sptr = NULL; + int ival; + + if (!(channels & (1 << chn))) + continue; + + if ((dir & 1) && snd_mixer_selem_has_playback_channel(elem, chn)) { + sptr = ptr; + if (!strncmp(ptr, "mute", 4) && snd_mixer_selem_has_playback_switch(elem)) { + snd_mixer_selem_get_playback_switch(elem, chn, &ival); + if (snd_mixer_selem_set_playback_switch(elem, chn, get_bool_simple(&ptr, "mute", 1, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "off", 3) && snd_mixer_selem_has_playback_switch(elem)) { + snd_mixer_selem_get_playback_switch(elem, chn, &ival); + if (snd_mixer_selem_set_playback_switch(elem, chn, get_bool_simple(&ptr, "off", 1, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "unmute", 6) && snd_mixer_selem_has_playback_switch(elem)) { + snd_mixer_selem_get_playback_switch(elem, chn, &ival); + if (snd_mixer_selem_set_playback_switch(elem, chn, get_bool_simple(&ptr, "unmute", 0, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "on", 2) && snd_mixer_selem_has_playback_switch(elem)) { + snd_mixer_selem_get_playback_switch(elem, chn, &ival); + if (snd_mixer_selem_set_playback_switch(elem, chn, get_bool_simple(&ptr, "on", 0, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "toggle", 6) && snd_mixer_selem_has_playback_switch(elem)) { + if (firstchn || !snd_mixer_selem_has_playback_switch_joined(elem)) { + snd_mixer_selem_get_playback_switch(elem, chn, &ival); + if (snd_mixer_selem_set_playback_switch(elem, chn, (ival ? 1 : 0) ^ 1) >= 0) + check_flag = 1; + } + simple_skip_word(&ptr, "toggle"); + } else if (isdigit(*ptr) || *ptr == '-' || *ptr == '+') { + if (set_volume_simple(elem, chn, &ptr, 0) >= 0) + check_flag = 1; + } else if (simple_skip_word(&ptr, "cap") || simple_skip_word(&ptr, "rec") || + simple_skip_word(&ptr, "nocap") || simple_skip_word(&ptr, "norec")) { + /* nothing */ + } else { + okflag &= ~1; + } + } + if ((dir & 2) && snd_mixer_selem_has_capture_channel(elem, chn)) { + if (sptr != NULL) + ptr = sptr; + sptr = ptr; + if (!strncmp(ptr, "cap", 3) && snd_mixer_selem_has_capture_switch(elem)) { + snd_mixer_selem_get_capture_switch(elem, chn, &ival); + if (snd_mixer_selem_set_capture_switch(elem, chn, get_bool_simple(&ptr, "cap", 0, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "rec", 3) && snd_mixer_selem_has_capture_switch(elem)) { + snd_mixer_selem_get_capture_switch(elem, chn, &ival); + if (snd_mixer_selem_set_capture_switch(elem, chn, get_bool_simple(&ptr, "rec", 0, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "nocap", 5) && snd_mixer_selem_has_capture_switch(elem)) { + snd_mixer_selem_get_capture_switch(elem, chn, &ival); + if (snd_mixer_selem_set_capture_switch(elem, chn, get_bool_simple(&ptr, "nocap", 1, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "norec", 5) && snd_mixer_selem_has_capture_switch(elem)) { + snd_mixer_selem_get_capture_switch(elem, chn, &ival); + if (snd_mixer_selem_set_capture_switch(elem, chn, get_bool_simple(&ptr, "norec", 1, ival)) >= 0) + check_flag = 1; + } else if (!strncmp(ptr, "toggle", 6) && snd_mixer_selem_has_capture_switch(elem)) { + if (firstchn || !snd_mixer_selem_has_capture_switch_joined(elem)) { + snd_mixer_selem_get_capture_switch(elem, chn, &ival); + if (snd_mixer_selem_set_capture_switch(elem, chn, (ival ? 1 : 0) ^ 1) >= 0) + check_flag = 1; + } + simple_skip_word(&ptr, "toggle"); + } else if (isdigit(*ptr) || *ptr == '-' || *ptr == '+') { + if (set_volume_simple(elem, chn, &ptr, 1) >= 0) + check_flag = 1; + } else if (simple_skip_word(&ptr, "mute") || simple_skip_word(&ptr, "off") || + simple_skip_word(&ptr, "unmute") || simple_skip_word(&ptr, "on")) { + /* nothing */ + } else { + okflag &= ~2; + } + } + if (okflag == 0) { + if (debugflag) { + if (dir & 1) + error("Unknown playback setup '%s'..", ptr); + if (dir & 2) + error("Unknown capture setup '%s'..", ptr); + } + return 0; /* just skip it */ + } + if (!multi) + ptr = optr; + firstchn = 0; + } + } + return check_flag; +} + +static int sset(unsigned int argc, char *argv[], int roflag, int keep_handle) +{ + int err = 0; + static snd_mixer_t *handle = NULL; + snd_mixer_elem_t *elem; + snd_mixer_selem_id_t *sid; + snd_mixer_selem_id_alloca(&sid); + + if (argc < 1) { + fprintf(stderr, "Specify a scontrol identifier: 'name',index\n"); + return 1; + } + if (parse_simple_id(argv[0], sid)) { + fprintf(stderr, "Wrong scontrol identifier: %s\n", argv[0]); + return 1; + } + if (!roflag && argc < 2) { + fprintf(stderr, "Specify what you want to set...\n"); + return 1; + } + if (handle == NULL) { + if ((err = snd_mixer_open(&handle, 0)) < 0) { + error("Mixer %s open error: %s\n", card, snd_strerror(err)); + return err; + } + if (smixer_level == 0 && (err = snd_mixer_attach(handle, card)) < 0) { + error("Mixer attach %s error: %s", card, snd_strerror(err)); + snd_mixer_close(handle); + handle = NULL; + return err; + } + if ((err = snd_mixer_selem_register(handle, smixer_level > 0 ? &smixer_options : NULL, NULL)) < 0) { + error("Mixer register error: %s", snd_strerror(err)); + snd_mixer_close(handle); + handle = NULL; + return err; + } + err = snd_mixer_load(handle); + if (err < 0) { + error("Mixer %s load error: %s", card, snd_strerror(err)); + snd_mixer_close(handle); + handle = NULL; + return err; + } + } + elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + if (ignore_error) + return 0; + error("Unable to find simple control '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); + snd_mixer_close(handle); + handle = NULL; + return -ENOENT; + } + if (!roflag) { + /* enum control */ + if (snd_mixer_selem_is_enumerated(elem)) + err = sset_enum(elem, argc, argv); + else + err = sset_channels(elem, argc, argv); + + if (!err) + goto done; + if (err < 0) { + error("Invalid command!"); + goto done; + } + } + if (!quiet) { + printf("Simple mixer control '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); + show_selem(handle, sid, " ", 1); + } + done: + if (! keep_handle) { + snd_mixer_close(handle); + handle = NULL; + } + return err < 0 ? 1 : 0; +} + +static void events_info(snd_hctl_elem_t *helem) +{ + snd_ctl_elem_id_t *id; + snd_ctl_elem_id_alloca(&id); + snd_hctl_elem_get_id(helem, id); + printf("event info: "); + show_control_id(id); + printf("\n"); +} + +static void events_value(snd_hctl_elem_t *helem) +{ + snd_ctl_elem_id_t *id; + snd_ctl_elem_id_alloca(&id); + snd_hctl_elem_get_id(helem, id); + printf("event value: "); + show_control_id(id); + printf("\n"); +} + +static void events_remove(snd_hctl_elem_t *helem) +{ + snd_ctl_elem_id_t *id; + snd_ctl_elem_id_alloca(&id); + snd_hctl_elem_get_id(helem, id); + printf("event remove: "); + show_control_id(id); + printf("\n"); +} + +static int element_callback(snd_hctl_elem_t *elem, unsigned int mask) +{ + if (mask == SND_CTL_EVENT_MASK_REMOVE) { + events_remove(elem); + return 0; + } + if (mask & SND_CTL_EVENT_MASK_INFO) + events_info(elem); + if (mask & SND_CTL_EVENT_MASK_VALUE) + events_value(elem); + return 0; +} + +static void events_add(snd_hctl_elem_t *helem) +{ + snd_ctl_elem_id_t *id; + snd_ctl_elem_id_alloca(&id); + snd_hctl_elem_get_id(helem, id); + printf("event add: "); + show_control_id(id); + printf("\n"); + snd_hctl_elem_set_callback(helem, element_callback); +} + +static int ctl_callback(snd_hctl_t *ctl ATTRIBUTE_UNUSED, unsigned int mask, + snd_hctl_elem_t *elem) +{ + if (mask & SND_CTL_EVENT_MASK_ADD) + events_add(elem); + return 0; +} + +static int events(int argc ATTRIBUTE_UNUSED, char *argv[] ATTRIBUTE_UNUSED) +{ + snd_hctl_t *handle; + snd_hctl_elem_t *helem; + int err; + + if ((err = snd_hctl_open(&handle, card, 0)) < 0) { + error("Control %s open error: %s\n", card, snd_strerror(err)); + return err; + } + snd_hctl_set_callback(handle, ctl_callback); + if ((err = snd_hctl_load(handle)) < 0) { + error("Control %s hbuild error: %s\n", card, snd_strerror(err)); + return err; + } + for (helem = snd_hctl_first_elem(handle); helem; helem = snd_hctl_elem_next(helem)) { + snd_hctl_elem_set_callback(helem, element_callback); + } + printf("Ready to listen...\n"); + while (1) { + int res = snd_hctl_wait(handle, -1); + if (res >= 0) { + printf("Poll ok: %i\n", res); + res = snd_hctl_handle_events(handle); + if (res < 0) + printf("ERR: %s (%d)\n", snd_strerror(res), res); + } + } + snd_hctl_close(handle); + return 0; +} + +static void sevents_value(snd_mixer_selem_id_t *sid) +{ + printf("event value: '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); +} + +static void sevents_info(snd_mixer_selem_id_t *sid) +{ + printf("event info: '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); +} + +static void sevents_remove(snd_mixer_selem_id_t *sid) +{ + printf("event remove: '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); +} + +static int melem_event(snd_mixer_elem_t *elem, unsigned int mask) +{ + snd_mixer_selem_id_t *sid; + snd_mixer_selem_id_alloca(&sid); + snd_mixer_selem_get_id(elem, sid); + if (mask == SND_CTL_EVENT_MASK_REMOVE) { + sevents_remove(sid); + return 0; + } + if (mask & SND_CTL_EVENT_MASK_INFO) + sevents_info(sid); + if (mask & SND_CTL_EVENT_MASK_VALUE) + sevents_value(sid); + return 0; +} + +static void sevents_add(snd_mixer_elem_t *elem) +{ + snd_mixer_selem_id_t *sid; + snd_mixer_selem_id_alloca(&sid); + snd_mixer_selem_get_id(elem, sid); + printf("event add: '%s',%i\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); + snd_mixer_elem_set_callback(elem, melem_event); +} + +static int mixer_event(snd_mixer_t *mixer ATTRIBUTE_UNUSED, unsigned int mask, + snd_mixer_elem_t *elem) +{ + if (mask & SND_CTL_EVENT_MASK_ADD) + sevents_add(elem); + return 0; +} + +static int sevents(int argc ATTRIBUTE_UNUSED, char *argv[] ATTRIBUTE_UNUSED) +{ + snd_mixer_t *handle; + int err; + + if ((err = snd_mixer_open(&handle, 0)) < 0) { + error("Mixer %s open error: %s", card, snd_strerror(err)); + return err; + } + if (smixer_level == 0 && (err = snd_mixer_attach(handle, card)) < 0) { + error("Mixer attach %s error: %s", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + if ((err = snd_mixer_selem_register(handle, smixer_level > 0 ? &smixer_options : NULL, NULL)) < 0) { + error("Mixer register error: %s", snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + snd_mixer_set_callback(handle, mixer_event); + err = snd_mixer_load(handle); + if (err < 0) { + error("Mixer %s load error: %s", card, snd_strerror(err)); + snd_mixer_close(handle); + return err; + } + + printf("Ready to listen...\n"); + while (1) { + int res; + res = snd_mixer_wait(handle, -1); + if (res >= 0) { + printf("Poll ok: %i\n", res); + res = snd_mixer_handle_events(handle); + assert(res >= 0); + } + } + snd_mixer_close(handle); + return 0; +} + +/* + * split a line into tokens + * the content in the line buffer is modified + */ +static int split_line(char *buf, char **token, int max_token) +{ + char *dst; + int n, esc, quote; + + for (n = 0; n < max_token; n++) { + while (isspace(*buf)) + buf++; + if (! *buf || *buf == '\n') + return n; + /* skip comments */ + if (*buf == '#' || *buf == '!') + return n; + esc = 0; + quote = 0; + token[n] = buf; + for (dst = buf; *buf && *buf != '\n'; buf++) { + if (esc) + esc = 0; + else if (isspace(*buf) && !quote) { + buf++; + break; + } else if (*buf == '\\') { + esc = 1; + continue; + } else if (*buf == '\'' || *buf == '"') { + if (! quote) { + quote = *buf; + continue; + } else if (*buf == quote) { + quote = 0; + continue; + } + } + *dst++ = *buf; + } + *dst = 0; + } + return n; +} + +#define MAX_ARGS 32 + +static int exec_stdin(void) +{ + int narg; + char buf[256], *args[MAX_ARGS]; + int err = 0; + + /* quiet = 1; */ + ignore_error = 1; + + while (fgets(buf, sizeof(buf), stdin)) { + narg = split_line(buf, args, MAX_ARGS); + if (narg > 0) { + if (!strcmp(args[0], "sset") || !strcmp(args[0], "set")) + err = sset(narg - 1, args + 1, 0, 1); + else if (!strcmp(args[0], "cset")) + err = cset(narg - 1, args + 1, 0, 1); + if (err < 0) + return 1; + } + } + return 0; +} + + +int main(int argc, char *argv[]) +{ + int badopt, retval, level = 0; + int read_stdin = 0; + static const struct option long_option[] = + { + {"help", 0, NULL, 'h'}, + {"card", 1, NULL, 'c'}, + {"device", 1, NULL, 'D'}, + {"quiet", 0, NULL, 'q'}, + {"inactive", 0, NULL, 'i'}, + {"debug", 0, NULL, 'd'}, + {"nocheck", 0, NULL, 'n'}, + {"version", 0, NULL, 'v'}, + {"abstract", 1, NULL, 'a'}, + {"stdin", 0, NULL, 's'}, + {"raw-volume", 0, NULL, 'R'}, + {"mapped-volume", 0, NULL, 'M'}, + {NULL, 0, NULL, 0}, + }; + + badopt = 0; + while (1) { + int c; + + if ((c = getopt_long(argc, argv, "hc:D:qidnva:sRM", long_option, NULL)) < 0) + break; + switch (c) { + case 'h': + help(); + return 0; + case 'c': + { + int i; + i = snd_card_get_index(optarg); + if (i >= 0 && i < 32) +#if defined(SND_LIB_VER) && SND_LIB_VER(1, 2, 5) <= SND_LIB_VERSION + sprintf(card, "sysdefault:%i", i); +#else + sprintf(card, "hw:%i", i); +#endif + else { + fprintf(stderr, "Invalid card number '%s'.\n", optarg); + badopt++; + } + } + break; + case 'D': + strncpy(card, optarg, sizeof(card)-1); + card[sizeof(card)-1] = '\0'; + break; + case 'q': + quiet = 1; + break; + case 'i': + level |= LEVEL_INACTIVE; + break; + case 'd': + debugflag = 1; + break; + case 'n': + no_check = 1; + break; + case 'v': + printf("amixer version " SND_UTIL_VERSION_STR "\n"); + return 0; + case 'a': + smixer_level = 1; + memset(&smixer_options, 0, sizeof(smixer_options)); + smixer_options.ver = 1; + if (!strcmp(optarg, "none")) + smixer_options.abstract = SND_MIXER_SABSTRACT_NONE; + else if (!strcmp(optarg, "basic")) + smixer_options.abstract = SND_MIXER_SABSTRACT_BASIC; + else { + fprintf(stderr, "Select correct abstraction level (none or basic)...\n"); + badopt++; + } + break; + case 's': + read_stdin = 1; + break; + case 'R': + std_vol_type = VOL_RAW; + break; + case 'M': + std_vol_type = VOL_MAP; + break; + default: + fprintf(stderr, "Invalid switch or option -%c needs an argument.\n", c); + badopt++; + } + } + if (badopt) + return 1; + + smixer_options.device = card; + + if (read_stdin) { + retval = exec_stdin(); + goto finish; + } + + if (argc - optind <= 0) { + retval = selems(LEVEL_BASIC | level) ? 1 : 0; + goto finish; + } + if (!strcmp(argv[optind], "help")) { + retval = help() ? 1 : 0; + } else if (!strcmp(argv[optind], "info")) { + retval = info() ? 1 : 0; + } else if (!strcmp(argv[optind], "controls")) { + retval = controls(level) ? 1 : 0; + } else if (!strcmp(argv[optind], "contents")) { + retval = controls(LEVEL_BASIC | level) ? 1 : 0; + } else if (!strcmp(argv[optind], "scontrols") || !strcmp(argv[optind], "simple")) { + retval = selems(level) ? 1 : 0; + } else if (!strcmp(argv[optind], "scontents")) { + retval = selems(LEVEL_BASIC | level) ? 1 : 0; + } else if (!strcmp(argv[optind], "sset") || !strcmp(argv[optind], "set")) { + retval = sset(argc - optind - 1, argc - optind > 1 ? argv + optind + 1 : NULL, 0, 0) ? 1 : 0; + } else if (!strcmp(argv[optind], "sget") || !strcmp(argv[optind], "get")) { + retval = sset(argc - optind - 1, argc - optind > 1 ? argv + optind + 1 : NULL, 1, 0) ? 1 : 0; + } else if (!strcmp(argv[optind], "cset")) { + retval = cset(argc - optind - 1, argc - optind > 1 ? argv + optind + 1 : NULL, 0, 0) ? 1 : 0; + } else if (!strcmp(argv[optind], "cget")) { + retval = cset(argc - optind - 1, argc - optind > 1 ? argv + optind + 1 : NULL, 1, 0) ? 1 : 0; + } else if (!strcmp(argv[optind], "events")) { + retval = events(argc - optind - 1, argc - optind > 1 ? argv + optind + 1 : NULL); + } else if (!strcmp(argv[optind], "sevents")) { + retval = sevents(argc - optind - 1, argc - optind > 1 ? argv + optind + 1 : NULL); + } else { + fprintf(stderr, "amixer: Unknown command '%s'...\n", argv[optind]); + retval = 0; + } + +finish: + snd_config_update_free_global(); + + return retval; +} diff --git a/amixer/amixer.h b/amixer/amixer.h new file mode 100644 index 0000000..af97628 --- /dev/null +++ b/amixer/amixer.h @@ -0,0 +1,21 @@ +/* + * ALSA command line mixer utility + * Copyright (c) 1999 by Jaroslav Kysela + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "../include/version.h" diff --git a/amixer/go b/amixer/go new file mode 100755 index 0000000..071627f --- /dev/null +++ b/amixer/go @@ -0,0 +1,10 @@ +#!/bin/bash + +#GDB="gdb --args" +GDB="" + +ALSA_MIXER_SIMPLE_MODULES="$HOME/hg/alsa-lib/modules/mixer/simple/.libs" \ +ALSA_MIXER_SIMPLE="$HOME/hg/alsa-lib/src/conf/smixer.conf" \ +ALSA_MIXER_SIMPLE_MPYTHON="$HOME/hg/alsa-lib/modules/mixer/simple/python/main.py" \ +LD_PRELOAD="$HOME/hg/alsa-lib/src/.libs/libasound.so" \ +$GDB ./amixer "$@" diff --git a/amixer/volume_mapping.c b/amixer/volume_mapping.c new file mode 120000 index 0000000..922aae8 --- /dev/null +++ b/amixer/volume_mapping.c @@ -0,0 +1 @@ +../alsamixer/volume_mapping.c \ No newline at end of file diff --git a/amixer/volume_mapping.h b/amixer/volume_mapping.h new file mode 120000 index 0000000..89718e7 --- /dev/null +++ b/amixer/volume_mapping.h @@ -0,0 +1 @@ +../alsamixer/volume_mapping.h \ No newline at end of file diff --git a/aplay/Makefile.am b/aplay/Makefile.am new file mode 100644 index 0000000..c3256e9 --- /dev/null +++ b/aplay/Makefile.am @@ -0,0 +1,31 @@ +LIBRT = @LIBRT@ + +AM_CPPFLAGS = -I$(top_srcdir)/include +LDADD = $(LIBINTL) $(LIBRT) + +# debug flags +#LDFLAGS = -static +#LDADD += -ldl + +bin_PROGRAMS = aplay +man_MANS = aplay.1 arecord.1 +noinst_HEADERS = formats.h + +EXTRA_DIST = aplay.1 arecord.1 +EXTRA_CLEAN = arecord + +arecord: aplay + rm -f arecord + $(LN_S) $< $@ + +arecord.1: aplay.1 + rm -f arecord.1 + $(LN_S) $< $@ + +install-exec-hook: arecord + rm -f $(DESTDIR)$(bindir)/arecord + (cd $(DESTDIR)$(bindir) && $(LN_S) aplay arecord) + +install-data-hook: + rm -f $(DESTDIR)$(mandir)/man1/arecord.1 + (cd $(DESTDIR)$(mandir)/man1 && $(LN_S) aplay.1 arecord.1) diff --git a/aplay/aplay.1 b/aplay/aplay.1 new file mode 100644 index 0000000..83d359d --- /dev/null +++ b/aplay/aplay.1 @@ -0,0 +1,267 @@ +.TH APLAY 1 "1 January 2010" +.SH NAME +arecord, aplay \- command\-line sound recorder and player for ALSA +soundcard driver +.SH SYNOPSIS +\fBarecord\fP [\fIflags\fP] [filename] +.br +\fBaplay\fP [\fIflags\fP] [filename [filename]] ... + +.SH DESCRIPTION +\fBarecord\fP is a command\-line soundfile recorder for the ALSA soundcard +driver. It supports several file formats and multiple soundcards with +multiple devices. If recording with interleaved mode samples the file is +automatically split before the 2GB filesize. + +\fBaplay\fP is much the same, only it plays instead of recording. For +supported soundfile formats, the sampling rate, bit depth, and so +forth can be automatically determined from the soundfile header. + +If filename is not specified, the standard output or input is used. The \fBaplay\fP utility accepts multiple filenames. + +.SH OPTIONS +.TP +\fI\-h, \-\-help\fP +Help: show syntax. +.TP +\fI\-\-version\fP +Print current version. +.TP +\fI\-l, \-\-list\-devices\fP +List all soundcards and digital audio devices +.TP +\fI\-L, \-\-list\-pcms\fP +List all PCMs defined +.TP +\fI\-D, \-\-device=NAME\fP +Select PCM by name +.TP +\fI\-q \-\-quiet\fP +Quiet mode. Suppress messages (not sound :)) +.TP +\fI\-t, \-\-file\-type TYPE\fP +File type (voc, wav, raw or au). +If this parameter is omitted the WAVE format is used. +.TP +\fI\-c, \-\-channels=#\fP +The number of channels. +The default is one channel. +Valid values are 1 through 32. +.TP +\fI\-f \-\-format=FORMAT\fP +Sample format +.br +Recognized sample formats are: S8 U8 S16_LE S16_BE U16_LE U16_BE S24_LE +S24_BE U24_LE U24_BE S32_LE S32_BE U32_LE U32_BE FLOAT_LE FLOAT_BE +FLOAT64_LE FLOAT64_BE IEC958_SUBFRAME_LE IEC958_SUBFRAME_BE MU_LAW +A_LAW IMA_ADPCM MPEG GSM SPECIAL S24_3LE S24_3BE U24_3LE U24_3BE S20_3LE +S20_3BE U20_3LE U20_3BE S18_3LE S18_3BE U18_3LE +.br +Some of these may not be available on selected hardware +.br +The available format shortcuts are: +.nf +\-f cd (16 bit little endian, 44100, stereo) [\-f S16_LE \-c2 \-r44100] +\-f cdr (16 bit big endian, 44100, stereo) [\-f S16_BE \-c2 \-r44100] +\-f dat (16 bit little endian, 48000, stereo) [\-f S16_LE \-c2 \-r48000] +.fi +If no format is given U8 is used. +.TP +\fI\-r, \-\-rate=#\fP +Sampling rate in Hertz. The default rate is 8000 Hertz. +If the value specified is less than 300, it is taken as the rate in kilohertz. +Valid values are 2000 through 192000 Hertz. +.TP +\fI\-d, \-\-duration=#\fP +Interrupt after # seconds. +A value of zero means infinity. +The default is zero, so if this option is omitted then the record/playback process will run until it is killed. +Either '-d' or '-s' option is available exclusively. +.TP +\fI\-s, \-\-samples=#\fP +Interrupt after transmission of # PCM frames. +A value of zero means infinity. +The default is zero, so if this options is omitted then the record/playback process will run until it is killed. +Either '-d' or '-s' option is available exclusively. +.TP +\fI\-M, \-\-mmap\fP +Use memory\-mapped (mmap) I/O mode for the audio stream. +If this option is not set, the read/write I/O mode will be used. +.TP +\fI\-N, \-\-nonblock\fP +Open the audio device in non\-blocking mode. If the device is busy the program will exit immediately. +If this option is not set the program will block until the audio device is available again. +.TP +\fI\-F, \-\-period\-time=#\fP +Distance between interrupts is # microseconds. +If no period time and no period size is given then a quarter of the buffer time is set. +.TP +\fI\-B, \-\-buffer\-time=#\fP +Buffer duration is # microseconds +If no buffer time and no buffer size is given then the maximal allowed buffer time but not more than 500ms is set. +.TP +\fI\-\-period\-size=#\fP +Distance between interrupts is # frames +If no period size and no period time is given then a quarter of the buffer size is set. +.TP +\fI\-\-buffer\-size=#\fP +Buffer duration is # frames +If no buffer time and no buffer size is given then the maximal allowed buffer time but not more than 500ms is set. +.TP +\fI\-A, \-\-avail\-min=#\fP +Min available space for wakeup is # microseconds +.TP +\fI\-R, \-\-start\-delay=#\fP +Delay for automatic PCM start is # microseconds +(relative to buffer size if <= 0) +.TP +\fI\-T, \-\-stop\-delay=#\fP +Delay for automatic PCM stop is # microseconds from xrun +.TP +\fI\-v, \-\-verbose\fP +Show PCM structure and setup. +This option is accumulative. The VU meter is displayed when this +is given twice or three times. +.TP +\fI\-V, \-\-vumeter=TYPE\fP +Specifies the VU\-meter type, either \fIstereo\fP or \fImono\fP. +The stereo VU\-meter is available only for 2\-channel stereo samples +with interleaved format. +.TP +\fI\-I, \-\-separate\-channels\fP +One file for each channel. This option disables max\-file\-time +and use\-strftime, and ignores SIGUSR1. The stereo VU meter is +not available with separate channels. +.TP +\fI\-P\fP +Playback. This is the default if the program is invoked +by typing aplay. +.TP +\fI\-C\fP +Record. This is the default if the program is invoked +by typing arecord. +.TP +\fI\-i, \-\-interactive\fP +Allow interactive operation via stdin. +Currently only pause/resume via space or enter key is implemented. +.TP +\fI-m, \-\-chmap=ch1,ch2,...\fP +Give the channel map to override or follow. Pass channel position +strings like \fIFL\fP, \fIFR\fP, etc. + +If a device supports the override of the channel map, \fBaplay\fP +tries to pass the given channel map. +If it doesn't support the channel map override but still it provides +the channel map information, \fBaplay\fP tries to rearrange the +channel order in the buffer to match with the returned channel map +from the device. +.TP +\fI\-\-disable\-resample\fP +Disable automatic rate resample. +.TP +\fI\-\-disable\-channels\fP +Disable automatic channel conversions. +.TP +\fI\-\-disable\-format\fP +Disable automatic format conversions. +.TP +\fI\-\-disable\-softvol\fP +Disable software volume control (softvol). +.TP +\fI\-\-test\-position\fP +Test ring buffer position. +.TP +\fI\-\-test\-coef=\fP +Test coefficient for ring buffer position; default is 8. +Expression for validation is: coef * (buffer_size / 2). +Minimum value is 1. +.TP +\fI\-\-test\-nowait\fP +Do not wait for the ring buffer \(hy eats the whole CPU. +.TP +\fI\-\-max\-file\-time\fP +While recording, when the output file has been accumulating +sound for this long, +close it and open a new output file. Default is the maximum +size supported by the file format: 2 GiB for WAV files. +This option has no effect if \-\-separate\-channels is +specified. +.TP +\fI\-\-process\-id\-file \fP +aplay writes its process ID here, so other programs can +send signals to it. +.TP +\fI\-\-use\-strftime\fP +When recording, interpret %\-codes in the file name parameter using +the strftime facility whenever the output file is opened. The +important strftime codes are: %Y is the year, %m month, %d day of +the month, %H hour, %M minute and %S second. In addition, %v is +the file number, starting at 1. When this option is specified, +intermediate directories for the output file are created automatically. +This option has no effect if \-\-separate\-channels is specified. +.TP +\fI\-\-dump\-hw\-params\fP +Dump hw_params of the device preconfigured status to stderr. The dump +lists capabilities of the selected device such as supported formats, +sampling rates, numbers of channels, period and buffer bytes/sizes/times. +For raw device hw:X this option basically lists hardware capabilities of +the soundcard. +.TP +\fI\-\-fatal\-errors\fP +Disables recovery attempts when errors (e.g. xrun) are encountered; the +aplay process instead aborts immediately. + +.SH SIGNALS +When recording, SIGINT, SIGTERM and SIGABRT will close the output +file and exit. SIGUSR1 will close the output file, open a new one, +and continue recording. However, SIGUSR1 does not work with +\-\-separate\-channels. + +.SH EXAMPLES + +.TP +\fBaplay \-c 1 \-t raw \-r 22050 \-f mu_law foobar\fR +will play the raw file "foobar" as a +22050\-Hz, mono, 8\-bit, Mu\-Law .au file. + +.TP +\fBarecord \-d 10 \-f cd \-t wav \-D copy foobar.wav\fP +will record foobar.wav as a 10\-second, CD\-quality wave file, using the +PCM "copy" (which might be defined in the user's .asoundrc file as: +.nf +pcm.copy { + type plug + slave { + pcm hw + } + route_policy copy +} +.fi + +.TP +\fBarecord \-t wav \-\-max\-file\-time 30 mon.wav\fP +Record from the default audio source in monaural, 8,000 samples +per second, 8 bits per sample. Start a new file every +30 seconds. File names are mon\-nn.wav, where nn increases +from 01. The file after mon\-99.wav is mon\-100.wav. + +.TP +\fBarecord \-f cd \-t wav \-\-max\-file\-time 3600 \-\-use-strftime %Y/%m/%d/listen-%H-%M-%v.wav\fP +Record in stereo from the default audio source. Create a new file +every hour. The files are placed in directories based on their start dates +and have names which include their start times and file numbers. + +.SH SEE ALSO +\fB +alsamixer(1), +amixer(1) +\fP + +.SH BUGS +Note that .aiff files are not currently supported. + +.SH AUTHOR +\fBarecord\fP and \fBaplay\fP are by Jaroslav Kysela +This document is by Paul Winkler . +Updated for Alsa 0.9 by James Tappin + diff --git a/aplay/aplay.c b/aplay/aplay.c new file mode 100644 index 0000000..c58c2e9 --- /dev/null +++ b/aplay/aplay.c @@ -0,0 +1,3564 @@ +/* + * aplay.c - plays and records + * + * CREATIVE LABS CHANNEL-files + * Microsoft WAVE-files + * SPARC AUDIO .AU-files + * Raw Data + * + * Copyright (c) by Jaroslav Kysela + * Based on vplay program by Michael Beck + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define _GNU_SOURCE +#include "aconfig.h" +#include +#include +#if HAVE_MALLOC_H +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gettext.h" +#include "formats.h" +#include "version.h" +#include "os_compat.h" + +#define ABS(a) (a) < 0 ? -(a) : (a) + +#ifdef SND_CHMAP_API_VERSION +#define CONFIG_SUPPORT_CHMAP 1 +#endif + +#ifndef LLONG_MAX +#define LLONG_MAX 9223372036854775807LL +#endif + +#ifndef le16toh +#include +#define le16toh(x) __le16_to_cpu(x) +#define be16toh(x) __be16_to_cpu(x) +#define le32toh(x) __le32_to_cpu(x) +#define be32toh(x) __be32_to_cpu(x) +#endif + +#define DEFAULT_FORMAT SND_PCM_FORMAT_U8 +#define DEFAULT_SPEED 8000 + +#define FORMAT_DEFAULT -1 +#define FORMAT_RAW 0 +#define FORMAT_VOC 1 +#define FORMAT_WAVE 2 +#define FORMAT_AU 3 + +/* global data */ + +static snd_pcm_sframes_t (*readi_func)(snd_pcm_t *handle, void *buffer, snd_pcm_uframes_t size); +static snd_pcm_sframes_t (*writei_func)(snd_pcm_t *handle, const void *buffer, snd_pcm_uframes_t size); +static snd_pcm_sframes_t (*readn_func)(snd_pcm_t *handle, void **bufs, snd_pcm_uframes_t size); +static snd_pcm_sframes_t (*writen_func)(snd_pcm_t *handle, void **bufs, snd_pcm_uframes_t size); + +enum { + VUMETER_NONE, + VUMETER_MONO, + VUMETER_STEREO +}; + +static char *command; +static snd_pcm_t *handle; +static struct { + snd_pcm_format_t format; + snd_pcm_subformat_t subformat; + unsigned int channels; + unsigned int rate; +} hwparams, rhwparams; +static int timelimit = 0; +static int sampleslimit = 0; +static int quiet_mode = 0; +static int file_type = FORMAT_DEFAULT; +static int open_mode = 0; +static snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK; +static int mmap_flag = 0; +static int interleaved = 1; +static int nonblock = 0; +static volatile sig_atomic_t in_aborting = 0; +static uint8_t *audiobuf = NULL; +static snd_pcm_uframes_t chunk_size = 0; +static unsigned period_time = 0; +static unsigned buffer_time = 0; +static snd_pcm_uframes_t period_frames = 0; +static snd_pcm_uframes_t buffer_frames = 0; +static int avail_min = -1; +static int start_delay = 0; +static int stop_delay = 0; +static int monotonic = 0; +static int interactive = 0; +static int can_pause = 0; +static int fatal_errors = 0; +static int verbose = 0; +static int vumeter = VUMETER_NONE; +static int buffer_pos = 0; +static size_t significant_bits_per_sample, bits_per_sample, bits_per_frame; +static size_t chunk_bytes; +static int test_position = 0; +static int test_coef = 8; +static int test_nowait = 0; +static snd_output_t *log; +static long long max_file_size = 0; +static int max_file_time = 0; +static int use_strftime = 0; +static volatile int recycle_capture_file = 0; +static long term_c_lflag = -1; +static int dump_hw_params = 0; + +static int fd = -1; +static off_t pbrec_count = LLONG_MAX, fdcount; +static int vocmajor, vocminor; + +static char *pidfile_name = NULL; +FILE *pidf = NULL; +static int pidfile_written = 0; + +#ifdef CONFIG_SUPPORT_CHMAP +static snd_pcm_chmap_t *channel_map = NULL; /* chmap to override */ +static unsigned int *hw_map = NULL; /* chmap to follow */ +#endif + +/* needed prototypes */ + +static void done_stdin(void); + +static void playback(char *filename); +static void capture(char *filename); +static void playbackv(char **filenames, unsigned int count); +static void capturev(char **filenames, unsigned int count); + +static void begin_voc(int fd, size_t count); +static void end_voc(int fd); +static void begin_wave(int fd, size_t count); +static void end_wave(int fd); +static void begin_au(int fd, size_t count); +static void end_au(int fd); + +static void suspend(void); + +static const struct fmt_capture { + void (*start) (int fd, size_t count); + void (*end) (int fd); + char *what; + long long max_filesize; +} fmt_rec_table[] = { + { NULL, NULL, N_("raw data"), LLONG_MAX }, + { begin_voc, end_voc, N_("VOC"), 16000000LL }, + /* FIXME: can WAV handle exactly 2GB or less than it? */ + { begin_wave, end_wave, N_("WAVE"), 2147483648LL }, + { begin_au, end_au, N_("Sparc Audio"), LLONG_MAX } +}; + +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95) +#define error(...) do {\ + fprintf(stderr, "%s: %s:%d: ", command, __func__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + putc('\n', stderr); \ +} while (0) +#else +#define error(args...) do {\ + fprintf(stderr, "%s: %s:%d: ", command, __func__, __LINE__); \ + fprintf(stderr, ##args); \ + putc('\n', stderr); \ +} while (0) +#endif + +static void usage(char *command) +{ + snd_pcm_format_t k; + printf( +_("Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +" --subformat=SUBFORMAT sample subformat (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default 8)\n" +" expression for validation is: coef * (buffer_size / 2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" + ) + , command); + printf(_("Recognized sample formats are:")); + for (k = 0; k <= SND_PCM_FORMAT_LAST; ++k) { + const char *s = snd_pcm_format_name(k); + if (s) + printf(" %s", s); + } + printf(_("\nSome of these may not be available on selected hardware\n")); + printf(_("The available format shortcuts are:\n")); + printf(_("-f cd (16 bit little endian, 44100, stereo)\n")); + printf(_("-f cdr (16 bit big endian, 44100, stereo)\n")); + printf(_("-f dat (16 bit little endian, 48000, stereo)\n")); +} + +static void device_list(void) +{ + snd_ctl_t *handle; + int card, err, dev, idx; + snd_ctl_card_info_t *info; + snd_pcm_info_t *pcminfo; + snd_ctl_card_info_alloca(&info); + snd_pcm_info_alloca(&pcminfo); + + card = -1; + if (snd_card_next(&card) < 0 || card < 0) { + error(_("no soundcards found...")); + return; + } + printf(_("**** List of %s Hardware Devices ****\n"), + snd_pcm_stream_name(stream)); + while (card >= 0) { + char name[32]; + sprintf(name, "hw:%d", card); + if ((err = snd_ctl_open(&handle, name, 0)) < 0) { + error("control open (%i): %s", card, snd_strerror(err)); + goto next_card; + } + if ((err = snd_ctl_card_info(handle, info)) < 0) { + error("control hardware info (%i): %s", card, snd_strerror(err)); + snd_ctl_close(handle); + goto next_card; + } + dev = -1; + while (1) { + unsigned int count; + if (snd_ctl_pcm_next_device(handle, &dev)<0) + error("snd_ctl_pcm_next_device"); + if (dev < 0) + break; + snd_pcm_info_set_device(pcminfo, dev); + snd_pcm_info_set_subdevice(pcminfo, 0); + snd_pcm_info_set_stream(pcminfo, stream); + if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) { + if (err != -ENOENT) + error("control digital audio info (%i): %s", card, snd_strerror(err)); + continue; + } + printf(_("card %i: %s [%s], device %i: %s [%s]\n"), + card, snd_ctl_card_info_get_id(info), snd_ctl_card_info_get_name(info), + dev, + snd_pcm_info_get_id(pcminfo), + snd_pcm_info_get_name(pcminfo)); + count = snd_pcm_info_get_subdevices_count(pcminfo); + printf( _(" Subdevices: %i/%i\n"), + snd_pcm_info_get_subdevices_avail(pcminfo), count); + for (idx = 0; idx < (int)count; idx++) { + snd_pcm_info_set_subdevice(pcminfo, idx); + if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) { + error("control digital audio playback info (%i): %s", card, snd_strerror(err)); + } else { + printf(_(" Subdevice #%i: %s\n"), + idx, snd_pcm_info_get_subdevice_name(pcminfo)); + } + } + } + snd_ctl_close(handle); + next_card: + if (snd_card_next(&card) < 0) { + error("snd_card_next"); + break; + } + } +} + +static void pcm_list(void) +{ + void **hints, **n; + char *name, *descr, *descr1, *io; + const char *filter; + + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return; + n = hints; + filter = stream == SND_PCM_STREAM_CAPTURE ? "Input" : "Output"; + while (*n != NULL) { + name = snd_device_name_get_hint(*n, "NAME"); + descr = snd_device_name_get_hint(*n, "DESC"); + io = snd_device_name_get_hint(*n, "IOID"); + if (io != NULL && strcmp(io, filter) != 0) + goto __end; + printf("%s\n", name); + if ((descr1 = descr) != NULL) { + printf(" "); + while (*descr1) { + if (*descr1 == '\n') + printf("\n "); + else + putchar(*descr1); + descr1++; + } + putchar('\n'); + } + __end: + if (name != NULL) + free(name); + if (descr != NULL) + free(descr); + if (io != NULL) + free(io); + n++; + } + snd_device_name_free_hint(hints); +} + +static void version(void) +{ + printf("%s: version " SND_UTIL_VERSION_STR " by Jaroslav Kysela \n", command); +} + +/* + * Subroutine to clean up before exit. + */ +static void prg_exit(int code) +{ + done_stdin(); + if (handle) + snd_pcm_close(handle); + if (pidfile_written) + remove (pidfile_name); + exit(code); +} + +static void signal_handler(int sig) +{ + if (in_aborting) + return; + + in_aborting = 1; + if (verbose==2) + putchar('\n'); + if (!quiet_mode) + fprintf(stderr, _("Aborted by signal %s...\n"), strsignal(sig)); + if (handle) + snd_pcm_abort(handle); + if (sig == SIGABRT) { + /* do not call snd_pcm_close() and abort immediately */ + handle = NULL; + prg_exit(EXIT_FAILURE); + } + signal(sig, SIG_DFL); +} + +/* call on SIGUSR1 signal. */ +static void signal_handler_recycle(int sig ATTRIBUTE_UNUSED) +{ + /* flag the capture loop to start a new output file */ + recycle_capture_file = 1; +} + +enum { + OPT_VERSION = 1, + OPT_PERIOD_SIZE, + OPT_BUFFER_SIZE, + OPT_DISABLE_RESAMPLE, + OPT_DISABLE_CHANNELS, + OPT_DISABLE_FORMAT, + OPT_DISABLE_SOFTVOL, + OPT_TEST_POSITION, + OPT_TEST_COEF, + OPT_TEST_NOWAIT, + OPT_MAX_FILE_TIME, + OPT_PROCESS_ID_FILE, + OPT_USE_STRFTIME, + OPT_DUMP_HWPARAMS, + OPT_FATAL_ERRORS, + OPT_SUBFORMAT, +}; + +/* + * make sure we write all bytes or return an error + */ +static ssize_t xwrite(int fd, const void *buf, size_t count) +{ + ssize_t written; + size_t offset = 0; + + while (offset < count) { + written = write(fd, (char *)buf + offset, count - offset); + if (written <= 0) + return written; + + offset += written; + }; + + return offset; +} + +static long parse_long(const char *str, int *err) +{ + long val; + char *endptr; + + errno = 0; + val = strtol(str, &endptr, 0); + + if (errno != 0 || *endptr != '\0') + *err = -1; + else + *err = 0; + + return val; +} + +int main(int argc, char *argv[]) +{ + int duration_or_sample = 0; + int option_index; + static const char short_options[] = "hnlLD:qt:c:f:r:d:s:MNF:A:R:T:B:vV:IPCi" +#ifdef CONFIG_SUPPORT_CHMAP + "m:" +#endif + ; + static const struct option long_options[] = { + {"help", 0, 0, 'h'}, + {"version", 0, 0, OPT_VERSION}, + {"list-devnames", 0, 0, 'n'}, + {"list-devices", 0, 0, 'l'}, + {"list-pcms", 0, 0, 'L'}, + {"device", 1, 0, 'D'}, + {"quiet", 0, 0, 'q'}, + {"file-type", 1, 0, 't'}, + {"channels", 1, 0, 'c'}, + {"format", 1, 0, 'f'}, + {"subformat", 1, 0, OPT_SUBFORMAT}, + {"rate", 1, 0, 'r'}, + {"duration", 1, 0 ,'d'}, + {"samples", 1, 0, 's'}, + {"mmap", 0, 0, 'M'}, + {"nonblock", 0, 0, 'N'}, + {"period-time", 1, 0, 'F'}, + {"period-size", 1, 0, OPT_PERIOD_SIZE}, + {"avail-min", 1, 0, 'A'}, + {"start-delay", 1, 0, 'R'}, + {"stop-delay", 1, 0, 'T'}, + {"buffer-time", 1, 0, 'B'}, + {"buffer-size", 1, 0, OPT_BUFFER_SIZE}, + {"verbose", 0, 0, 'v'}, + {"vumeter", 1, 0, 'V'}, + {"separate-channels", 0, 0, 'I'}, + {"playback", 0, 0, 'P'}, + {"capture", 0, 0, 'C'}, + {"disable-resample", 0, 0, OPT_DISABLE_RESAMPLE}, + {"disable-channels", 0, 0, OPT_DISABLE_CHANNELS}, + {"disable-format", 0, 0, OPT_DISABLE_FORMAT}, + {"disable-softvol", 0, 0, OPT_DISABLE_SOFTVOL}, + {"test-position", 0, 0, OPT_TEST_POSITION}, + {"test-coef", 1, 0, OPT_TEST_COEF}, + {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, + {"max-file-time", 1, 0, OPT_MAX_FILE_TIME}, + {"process-id-file", 1, 0, OPT_PROCESS_ID_FILE}, + {"use-strftime", 0, 0, OPT_USE_STRFTIME}, + {"interactive", 0, 0, 'i'}, + {"dump-hw-params", 0, 0, OPT_DUMP_HWPARAMS}, + {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, +#ifdef CONFIG_SUPPORT_CHMAP + {"chmap", 1, 0, 'm'}, +#endif + {0, 0, 0, 0} + }; + char *pcm_name = "default"; + int tmp, err, c; + int do_device_list = 0, do_pcm_list = 0, force_sample_format = 0; + snd_pcm_info_t *info; + FILE *direction; + +#ifdef ENABLE_NLS + setlocale(LC_ALL, ""); + textdomain(PACKAGE); +#endif + + snd_pcm_info_alloca(&info); + + err = snd_output_stdio_attach(&log, stderr, 0); + assert(err >= 0); + + command = argv[0]; + file_type = FORMAT_DEFAULT; + if (strstr(argv[0], "arecord")) { + stream = SND_PCM_STREAM_CAPTURE; + file_type = FORMAT_WAVE; + command = "arecord"; + start_delay = 1; + direction = stdout; + } else if (strstr(argv[0], "aplay")) { + stream = SND_PCM_STREAM_PLAYBACK; + command = "aplay"; + direction = stdin; + } else { + error(_("command should be named either arecord or aplay")); + return 1; + } + + if (isatty(fileno(direction)) && (argc == 1)) { + usage(command); + return 1; + } + + chunk_size = -1; + rhwparams.format = DEFAULT_FORMAT; + rhwparams.subformat = SND_PCM_SUBFORMAT_STD; + rhwparams.rate = DEFAULT_SPEED; + rhwparams.channels = 1; + + while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) { + switch (c) { + case 'h': + usage(command); + return 0; + case OPT_VERSION: + version(); + return 0; + case 'l': + do_device_list = 1; + break; + case 'L': + do_pcm_list = 1; + break; + case 'D': + pcm_name = optarg; + break; + case 'q': + quiet_mode = 1; + break; + case 't': + if (strcasecmp(optarg, "raw") == 0) + file_type = FORMAT_RAW; + else if (strcasecmp(optarg, "voc") == 0) + file_type = FORMAT_VOC; + else if (strcasecmp(optarg, "wav") == 0) + file_type = FORMAT_WAVE; + else if (strcasecmp(optarg, "au") == 0 || strcasecmp(optarg, "sparc") == 0) + file_type = FORMAT_AU; + else { + error(_("unrecognized file format %s"), optarg); + return 1; + } + break; + case 'c': + rhwparams.channels = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid channels argument '%s'"), optarg); + return 1; + } + if (rhwparams.channels < 1 || rhwparams.channels > 256) { + error(_("value %i for channels is invalid"), rhwparams.channels); + return 1; + } + break; + case 'f': + force_sample_format = 1; + if (strcasecmp(optarg, "cd") == 0 || strcasecmp(optarg, "cdr") == 0) { + if (strcasecmp(optarg, "cdr") == 0) + rhwparams.format = SND_PCM_FORMAT_S16_BE; + else + rhwparams.format = file_type == FORMAT_AU ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_S16_LE; + rhwparams.rate = 44100; + rhwparams.channels = 2; + } else if (strcasecmp(optarg, "dat") == 0) { + rhwparams.format = file_type == FORMAT_AU ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_S16_LE; + rhwparams.rate = 48000; + rhwparams.channels = 2; + } else { + rhwparams.format = snd_pcm_format_value(optarg); + if (rhwparams.format == SND_PCM_FORMAT_UNKNOWN) { + error(_("wrong extended format '%s'"), optarg); + prg_exit(EXIT_FAILURE); + } + } + break; + case OPT_SUBFORMAT: +#if SND_LIB_VER(1, 2, 10) < SND_LIB_VERSION + rhwparams.subformat = snd_pcm_subformat_value(optarg); + if (rhwparams.subformat == SND_PCM_SUBFORMAT_UNKNOWN) { +#else + if (strcasecmp(optarg, "std") != 0 && strcasecmp(optarg, "standard") != 0) { +#endif + error(_("wrong extended subformat '%s'"), optarg); + prg_exit(EXIT_FAILURE); + } + break; + case 'r': + tmp = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid rate argument '%s'"), optarg); + return 1; + } + if (tmp < 1000) + tmp *= 1000; + rhwparams.rate = tmp; + if (tmp < 2000 || tmp > 768000) { + error(_("bad speed value %i"), tmp); + return 1; + } + break; + case 'd': + if (duration_or_sample) { + error(_("duration and samples arguments cannot be used together")); + return 1; + } + timelimit = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid duration argument '%s'"), optarg); + return 1; + } + duration_or_sample = 1; + break; + case 's': + if (duration_or_sample) { + error(_("samples and duration arguments cannot be used together")); + return 1; + } + sampleslimit = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid samples argument '%s'"), optarg); + return 1; + } + duration_or_sample = 1; + break; + case 'N': + nonblock = 1; + open_mode |= SND_PCM_NONBLOCK; + break; + case 'F': + period_time = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid period time argument '%s'"), optarg); + return 1; + } + break; + case 'B': + buffer_time = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid buffer time argument '%s'"), optarg); + return 1; + } + break; + case OPT_PERIOD_SIZE: + period_frames = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid period size argument '%s'"), optarg); + return 1; + } + break; + case OPT_BUFFER_SIZE: + buffer_frames = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid buffer size argument '%s'"), optarg); + return 1; + } + break; + case 'A': + avail_min = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid min available space argument '%s'"), optarg); + return 1; + } + break; + case 'R': + start_delay = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid start delay argument '%s'"), optarg); + return 1; + } + break; + case 'T': + stop_delay = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid stop delay argument '%s'"), optarg); + return 1; + } + break; + case 'v': + verbose++; + if (verbose > 1 && !vumeter) + vumeter = VUMETER_MONO; + break; + case 'V': + if (*optarg == 's') + vumeter = VUMETER_STEREO; + else if (*optarg == 'm') + vumeter = VUMETER_MONO; + else + vumeter = VUMETER_NONE; + break; + case 'M': + mmap_flag = 1; + break; + case 'I': + interleaved = 0; + break; + case 'P': + stream = SND_PCM_STREAM_PLAYBACK; + command = "aplay"; + break; + case 'C': + stream = SND_PCM_STREAM_CAPTURE; + command = "arecord"; + start_delay = 1; + if (file_type == FORMAT_DEFAULT) + file_type = FORMAT_WAVE; + break; + case 'i': + interactive = 1; + break; + case OPT_DISABLE_RESAMPLE: + open_mode |= SND_PCM_NO_AUTO_RESAMPLE; + break; + case OPT_DISABLE_CHANNELS: + open_mode |= SND_PCM_NO_AUTO_CHANNELS; + break; + case OPT_DISABLE_FORMAT: + open_mode |= SND_PCM_NO_AUTO_FORMAT; + break; + case OPT_DISABLE_SOFTVOL: + open_mode |= SND_PCM_NO_SOFTVOL; + break; + case OPT_TEST_POSITION: + test_position = 1; + break; + case OPT_TEST_COEF: + test_coef = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid test coef argument '%s'"), optarg); + return 1; + } + if (test_coef < 1) + test_coef = 1; + break; + case OPT_TEST_NOWAIT: + test_nowait = 1; + break; + case OPT_MAX_FILE_TIME: + max_file_time = parse_long(optarg, &err); + if (err < 0) { + error(_("invalid max file time argument '%s'"), optarg); + return 1; + } + break; + case OPT_PROCESS_ID_FILE: + pidfile_name = optarg; + break; + case OPT_USE_STRFTIME: + use_strftime = 1; + break; + case OPT_DUMP_HWPARAMS: + dump_hw_params = 1; + break; + case OPT_FATAL_ERRORS: + fatal_errors = 1; + break; +#ifdef CONFIG_SUPPORT_CHMAP + case 'm': + channel_map = snd_pcm_chmap_parse_string(optarg); + if (!channel_map) { + fprintf(stderr, _("Unable to parse channel map string: %s\n"), optarg); + return 1; + } + break; +#endif + default: + fprintf(stderr, _("Try `%s --help' for more information.\n"), command); + return 1; + } + } + + if (do_device_list) { + if (do_pcm_list) pcm_list(); + device_list(); + goto __end; + } else if (do_pcm_list) { + pcm_list(); + goto __end; + } + + err = snd_pcm_open(&handle, pcm_name, stream, open_mode); + if (err < 0) { + error(_("audio open error: %s"), snd_strerror(err)); + return 1; + } + + if ((err = snd_pcm_info(handle, info)) < 0) { + error(_("info error: %s"), snd_strerror(err)); + return 1; + } + + if (nonblock) { + err = snd_pcm_nonblock(handle, 1); + if (err < 0) { + error(_("nonblock setting error: %s"), snd_strerror(err)); + return 1; + } + } + + if (!force_sample_format && + isatty(fileno(stdin)) && + stream == SND_PCM_STREAM_CAPTURE && + snd_pcm_format_width(rhwparams.format) <= 8) + fprintf(stderr, "Warning: Some sources (like microphones) may produce inaudible results\n" + " with 8-bit sampling. Use '-f' argument to increase resolution\n" + " e.g. '-f S16_LE'.\n"); + + chunk_size = 1024; + hwparams = rhwparams; + + audiobuf = (uint8_t *)malloc(1024); + if (audiobuf == NULL) { + error(_("not enough memory")); + return 1; + } + + if (mmap_flag) { + writei_func = snd_pcm_mmap_writei; + readi_func = snd_pcm_mmap_readi; + writen_func = snd_pcm_mmap_writen; + readn_func = snd_pcm_mmap_readn; + } else { + writei_func = snd_pcm_writei; + readi_func = snd_pcm_readi; + writen_func = snd_pcm_writen; + readn_func = snd_pcm_readn; + } + + if (pidfile_name) { + errno = 0; + pidf = fopen (pidfile_name, "w"); + if (pidf) { + (void)fprintf (pidf, "%d\n", getpid()); + fclose(pidf); + pidfile_written = 1; + } else { + error(_("Cannot create process ID file %s: %s"), + pidfile_name, strerror (errno)); + return 1; + } + } + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + signal(SIGABRT, signal_handler); + signal(SIGUSR1, signal_handler_recycle); + if (interleaved) { + if (optind > argc - 1) { + if (stream == SND_PCM_STREAM_PLAYBACK) + playback(NULL); + else + capture(NULL); + } else { + while (optind <= argc - 1) { + if (stream == SND_PCM_STREAM_PLAYBACK) + playback(argv[optind++]); + else + capture(argv[optind++]); + } + } + } else { + if (stream == SND_PCM_STREAM_PLAYBACK) + playbackv(&argv[optind], argc - optind); + else + capturev(&argv[optind], argc - optind); + } + if (verbose==2) + putchar('\n'); + snd_pcm_close(handle); + handle = NULL; + free(audiobuf); + __end: + snd_output_close(log); + snd_config_update_free_global(); + prg_exit(EXIT_SUCCESS); + /* avoid warning */ + return EXIT_SUCCESS; +} + +/* + * Safe read (for pipes) + */ + +static ssize_t safe_read(int fd, void *buf, size_t count) +{ + ssize_t result = 0, res; + + while (count > 0 && !in_aborting) { + if ((res = read(fd, buf, count)) == 0) + break; + if (res < 0) + return result > 0 ? result : res; + count -= res; + result += res; + buf = (char *)buf + res; + } + return result; +} + +/* + * Test, if it is a .VOC file and return >=0 if ok (this is the length of rest) + * < 0 if not + */ +static int test_vocfile(void *buffer) +{ + VocHeader *vp = buffer; + + if (!memcmp(vp->magic, VOC_MAGIC_STRING, 20)) { + vocminor = LE_SHORT(vp->version) & 0xFF; + vocmajor = LE_SHORT(vp->version) / 256; + if (LE_SHORT(vp->version) != (0x1233 - LE_SHORT(vp->coded_ver))) + return -2; /* coded version mismatch */ + return LE_SHORT(vp->headerlen) - sizeof(VocHeader); /* 0 mostly */ + } + return -1; /* magic string fail */ +} + +/* + * helper for test_wavefile + */ + +static size_t test_wavefile_read(int fd, uint8_t *buffer, size_t *size, size_t reqsize, int line) +{ + if (*size >= reqsize) + return *size; + if ((size_t)safe_read(fd, buffer + *size, reqsize - *size) != reqsize - *size) { + error(_("read error (called from line %i)"), line); + prg_exit(EXIT_FAILURE); + } + return *size = reqsize; +} + +#define check_wavefile_space(buffer, len, blimit) \ + if (len > blimit) { \ + blimit = len; \ + if ((buffer = realloc(buffer, blimit)) == NULL) { \ + error(_("not enough memory")); \ + prg_exit(EXIT_FAILURE); \ + } \ + } + +/* + * test, if it's a .WAV file, > 0 if ok (and set the speed, stereo etc.) + * == 0 if not + * Value returned is bytes to be discarded. + */ +static ssize_t test_wavefile(int fd, uint8_t *_buffer, size_t size) +{ + WaveHeader *h = (WaveHeader *)_buffer; + uint8_t *buffer = NULL; + size_t blimit = 0; + WaveFmtBody *f; + WaveChunkHeader *c; + uint32_t type, len; + unsigned short format, channels; + int big_endian, native_format; + uint8_t vbps = 0; + + if (size < sizeof(WaveHeader)) + return -1; + if (h->magic == WAV_RIFF) + big_endian = 0; + else if (h->magic == WAV_RIFX) + big_endian = 1; + else + return -1; + if (h->type != WAV_WAVE) + return -1; + + if (size > sizeof(WaveHeader)) { + check_wavefile_space(buffer, size - sizeof(WaveHeader), blimit); + memcpy(buffer, _buffer + sizeof(WaveHeader), size - sizeof(WaveHeader)); + } + size -= sizeof(WaveHeader); + while (1) { + check_wavefile_space(buffer, sizeof(WaveChunkHeader), blimit); + test_wavefile_read(fd, buffer, &size, sizeof(WaveChunkHeader), __LINE__); + c = (WaveChunkHeader*)buffer; + type = c->type; + len = TO_CPU_INT(c->length, big_endian); + len += len % 2; + if (size > sizeof(WaveChunkHeader)) + memmove(buffer, buffer + sizeof(WaveChunkHeader), size - sizeof(WaveChunkHeader)); + size -= sizeof(WaveChunkHeader); + if (type == WAV_FMT) + break; + check_wavefile_space(buffer, len, blimit); + test_wavefile_read(fd, buffer, &size, len, __LINE__); + if (size > len) + memmove(buffer, buffer + len, size - len); + size -= len; + } + + if (len < sizeof(WaveFmtBody)) { + error(_("unknown length of 'fmt ' chunk (read %u, should be %u at least)"), + len, (uint32_t)sizeof(WaveFmtBody)); + prg_exit(EXIT_FAILURE); + } + check_wavefile_space(buffer, len, blimit); + test_wavefile_read(fd, buffer, &size, len, __LINE__); + f = (WaveFmtBody*) buffer; + format = TO_CPU_SHORT(f->format, big_endian); + if (format == WAV_FMT_EXTENSIBLE) { + WaveFmtExtensibleBody *fe = (WaveFmtExtensibleBody*)buffer; + if (len < sizeof(WaveFmtExtensibleBody)) { + error(_("unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)"), + len, (unsigned int)sizeof(WaveFmtExtensibleBody)); + prg_exit(EXIT_FAILURE); + } + if (memcmp(fe->guid_tag, WAV_GUID_TAG, 14) != 0) { + error(_("wrong format tag in extensible 'fmt ' chunk")); + prg_exit(EXIT_FAILURE); + } + vbps = TO_CPU_SHORT(fe->bit_p_spl, big_endian); + format = TO_CPU_SHORT(fe->guid_format, big_endian); + } + if (format != WAV_FMT_PCM && + format != WAV_FMT_IEEE_FLOAT) { + error(_("can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded"), format); + prg_exit(EXIT_FAILURE); + } + channels = TO_CPU_SHORT(f->channels, big_endian); + if (channels < 1) { + error(_("can't play WAVE-files with %d tracks"), channels); + prg_exit(EXIT_FAILURE); + } + hwparams.channels = channels; + if (vbps > TO_CPU_SHORT(f->bit_p_spl, big_endian)) { + error(_("valid bps greater than bps: %d > %d"), vbps, TO_CPU_SHORT(f->bit_p_spl, big_endian)); + prg_exit(EXIT_FAILURE); + } + switch (TO_CPU_SHORT(f->bit_p_spl, big_endian)) { + case 8: + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != SND_PCM_FORMAT_U8) + fprintf(stderr, _("Warning: format is changed to U8\n")); + hwparams.format = SND_PCM_FORMAT_U8; + break; + case 16: + if (big_endian) + native_format = SND_PCM_FORMAT_S16_BE; + else + native_format = SND_PCM_FORMAT_S16_LE; + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != native_format) + fprintf(stderr, _("Warning: format is changed to %s\n"), + snd_pcm_format_name(native_format)); + hwparams.format = native_format; + break; + case 24: + switch (TO_CPU_SHORT(f->byte_p_spl, big_endian) / hwparams.channels) { + case 3: + if (big_endian) + native_format = SND_PCM_FORMAT_S24_3BE; + else + native_format = SND_PCM_FORMAT_S24_3LE; + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != native_format) + fprintf(stderr, _("Warning: format is changed to %s\n"), + snd_pcm_format_name(native_format)); + hwparams.format = native_format; + break; + case 4: + if (big_endian) + native_format = SND_PCM_FORMAT_S24_BE; + else + native_format = SND_PCM_FORMAT_S24_LE; + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != native_format) + fprintf(stderr, _("Warning: format is changed to %s\n"), + snd_pcm_format_name(native_format)); + hwparams.format = native_format; + break; + default: + error(_(" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)"), + TO_CPU_SHORT(f->bit_p_spl, big_endian), + TO_CPU_SHORT(f->byte_p_spl, big_endian), + hwparams.channels); + prg_exit(EXIT_FAILURE); + } + break; + case 32: + if (format == WAV_FMT_PCM) { + switch (vbps) { + case 24: + if (big_endian) + native_format = SND_PCM_FORMAT_S24_BE; + else + native_format = SND_PCM_FORMAT_S24_LE; + break; + default: + if (big_endian) + native_format = SND_PCM_FORMAT_S32_BE; + else + native_format = SND_PCM_FORMAT_S32_LE; + break; + } + hwparams.format = native_format; + } else if (format == WAV_FMT_IEEE_FLOAT) { + if (big_endian) + native_format = SND_PCM_FORMAT_FLOAT_BE; + else + native_format = SND_PCM_FORMAT_FLOAT_LE; + hwparams.format = native_format; + } + break; + default: + error(_(" can't play WAVE-files with sample %d bits wide"), + TO_CPU_SHORT(f->bit_p_spl, big_endian)); + prg_exit(EXIT_FAILURE); + } + hwparams.rate = TO_CPU_INT(f->sample_fq, big_endian); + + if (size > len) + memmove(buffer, buffer + len, size - len); + size -= len; + + while (1) { + uint32_t type, len; + + check_wavefile_space(buffer, sizeof(WaveChunkHeader), blimit); + test_wavefile_read(fd, buffer, &size, sizeof(WaveChunkHeader), __LINE__); + c = (WaveChunkHeader*)buffer; + type = c->type; + len = TO_CPU_INT(c->length, big_endian); + if (size > sizeof(WaveChunkHeader)) + memmove(buffer, buffer + sizeof(WaveChunkHeader), size - sizeof(WaveChunkHeader)); + size -= sizeof(WaveChunkHeader); + if (type == WAV_DATA) { + if (len < pbrec_count && len < 0x7ffffffe) + pbrec_count = len; + if (size > 0) + memcpy(_buffer, buffer, size); + free(buffer); + return size; + } + len += len % 2; + check_wavefile_space(buffer, len, blimit); + test_wavefile_read(fd, buffer, &size, len, __LINE__); + if (size > len) + memmove(buffer, buffer + len, size - len); + size -= len; + } + + /* shouldn't be reached */ + return -1; +} + +/* + + */ + +static int test_au(int fd, void *buffer) +{ + AuHeader *ap = buffer; + + if (ap->magic != AU_MAGIC) + return -1; + if (BE_INT(ap->hdr_size) > 128 || BE_INT(ap->hdr_size) < 24) + return -1; + pbrec_count = BE_INT(ap->data_size); + switch (BE_INT(ap->encoding)) { + case AU_FMT_ULAW: + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != SND_PCM_FORMAT_MU_LAW) + fprintf(stderr, _("Warning: format is changed to MU_LAW\n")); + hwparams.format = SND_PCM_FORMAT_MU_LAW; + break; + case AU_FMT_LIN8: + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != SND_PCM_FORMAT_U8) + fprintf(stderr, _("Warning: format is changed to U8\n")); + hwparams.format = SND_PCM_FORMAT_U8; + break; + case AU_FMT_LIN16: + if (hwparams.format != DEFAULT_FORMAT && + hwparams.format != SND_PCM_FORMAT_S16_BE) + fprintf(stderr, _("Warning: format is changed to S16_BE\n")); + hwparams.format = SND_PCM_FORMAT_S16_BE; + break; + default: + return -1; + } + hwparams.rate = BE_INT(ap->sample_rate); + if (hwparams.rate < 2000 || hwparams.rate > 256000) + return -1; + hwparams.channels = BE_INT(ap->channels); + if (hwparams.channels < 1 || hwparams.channels > 256) + return -1; + if ((size_t)safe_read(fd, (char *)buffer + sizeof(AuHeader), BE_INT(ap->hdr_size) - sizeof(AuHeader)) != BE_INT(ap->hdr_size) - sizeof(AuHeader)) { + error(_("read error")); + prg_exit(EXIT_FAILURE); + } + return 0; +} + +static void show_available_sample_formats(snd_pcm_hw_params_t* params) +{ + snd_pcm_format_t format; + + fprintf(stderr, "Available formats:\n"); + for (format = 0; format <= SND_PCM_FORMAT_LAST; format++) { + if (snd_pcm_hw_params_test_format(handle, params, format) == 0) + fprintf(stderr, "- %s\n", snd_pcm_format_name(format)); + } +} + +#ifdef CONFIG_SUPPORT_CHMAP +static int setup_chmap(void) +{ + snd_pcm_chmap_t *chmap = channel_map; + char mapped[hwparams.channels]; + snd_pcm_chmap_t *hw_chmap; + unsigned int ch, i; + int err; + + if (!chmap) + return 0; + + if (chmap->channels != hwparams.channels) { + error(_("Channel numbers don't match between hw_params and channel map")); + return -1; + } + err = snd_pcm_set_chmap(handle, chmap); + if (!err) + return 0; + + hw_chmap = snd_pcm_get_chmap(handle); + if (!hw_chmap) { + fprintf(stderr, _("Warning: unable to get channel map\n")); + return 0; + } + + if (hw_chmap->channels == chmap->channels && + !memcmp(hw_chmap, chmap, 4 * (chmap->channels + 1))) { + /* maps are identical, so no need to convert */ + free(hw_chmap); + return 0; + } + + hw_map = calloc(hwparams.channels, sizeof(int)); + if (!hw_map) { + error(_("not enough memory")); + free(hw_chmap); + return -1; + } + + memset(mapped, 0, sizeof(mapped)); + for (ch = 0; ch < hw_chmap->channels; ch++) { + if (chmap->pos[ch] == hw_chmap->pos[ch]) { + mapped[ch] = 1; + hw_map[ch] = ch; + continue; + } + for (i = 0; i < hw_chmap->channels; i++) { + if (!mapped[i] && chmap->pos[ch] == hw_chmap->pos[i]) { + mapped[i] = 1; + hw_map[ch] = i; + break; + } + } + if (i >= hw_chmap->channels) { + char buf[256]; + error(_("Channel %d doesn't match with hw_params"), ch); + snd_pcm_chmap_print(hw_chmap, sizeof(buf), buf); + fprintf(stderr, "hardware chmap = %s\n", buf); + free(hw_chmap); + return -1; + } + } + free(hw_chmap); + return 0; +} +#else +#define setup_chmap() 0 +#endif + +static void set_params(void) +{ + snd_pcm_hw_params_t *params; + snd_pcm_sw_params_t *swparams; + snd_pcm_uframes_t buffer_size; + int err; + size_t n; + unsigned int rate; + snd_pcm_uframes_t start_threshold, stop_threshold; + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_sw_params_alloca(&swparams); + err = snd_pcm_hw_params_any(handle, params); + if (err < 0) { + error(_("Broken configuration for this PCM: no configurations available")); + prg_exit(EXIT_FAILURE); + } + if (dump_hw_params) { + fprintf(stderr, _("HW Params of device \"%s\":\n"), + snd_pcm_name(handle)); + fprintf(stderr, "--------------------\n"); + snd_pcm_hw_params_dump(params, log); + fprintf(stderr, "--------------------\n"); + } + if (mmap_flag) { + snd_pcm_access_mask_t *mask = alloca(snd_pcm_access_mask_sizeof()); + snd_pcm_access_mask_none(mask); + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_INTERLEAVED); + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED); + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_COMPLEX); + err = snd_pcm_hw_params_set_access_mask(handle, params, mask); + } else if (interleaved) + err = snd_pcm_hw_params_set_access(handle, params, + SND_PCM_ACCESS_RW_INTERLEAVED); + else + err = snd_pcm_hw_params_set_access(handle, params, + SND_PCM_ACCESS_RW_NONINTERLEAVED); + if (err < 0) { + error(_("Access type not available")); + prg_exit(EXIT_FAILURE); + } + err = snd_pcm_hw_params_set_format(handle, params, hwparams.format); + if (err < 0) { + error(_("Sample format non available")); + show_available_sample_formats(params); + prg_exit(EXIT_FAILURE); + } + err = snd_pcm_hw_params_set_subformat(handle, params, hwparams.subformat); + if (err < 0) { + error(_("Sample subformat not available")); + prg_exit(EXIT_FAILURE); + } + err = snd_pcm_hw_params_set_channels(handle, params, hwparams.channels); + if (err < 0) { + error(_("Channels count non available")); + prg_exit(EXIT_FAILURE); + } + +#if 0 + err = snd_pcm_hw_params_set_periods_min(handle, params, 2); + assert(err >= 0); +#endif + rate = hwparams.rate; + err = snd_pcm_hw_params_set_rate_near(handle, params, &hwparams.rate, 0); + assert(err >= 0); + if ((float)rate * 1.05 < hwparams.rate || (float)rate * 0.95 > hwparams.rate) { + if (!quiet_mode) { + char plugex[64]; + const char *pcmname = snd_pcm_name(handle); + fprintf(stderr, _("Warning: rate is not accurate (requested = %iHz, got = %iHz)\n"), rate, hwparams.rate); + if (! pcmname || strchr(snd_pcm_name(handle), ':')) + *plugex = 0; + else + snprintf(plugex, sizeof(plugex), "(-Dplug:%s)", + snd_pcm_name(handle)); + fprintf(stderr, _(" please, try the plug plugin %s\n"), + plugex); + } + } + rate = hwparams.rate; + if (buffer_time == 0 && buffer_frames == 0) { + err = snd_pcm_hw_params_get_buffer_time_max(params, + &buffer_time, 0); + assert(err >= 0); + if (buffer_time > 500000) + buffer_time = 500000; + } + if (period_time == 0 && period_frames == 0) { + if (buffer_time > 0) + period_time = buffer_time / 4; + else + period_frames = buffer_frames / 4; + } + if (period_time > 0) + err = snd_pcm_hw_params_set_period_time_near(handle, params, + &period_time, 0); + else + err = snd_pcm_hw_params_set_period_size_near(handle, params, + &period_frames, 0); + assert(err >= 0); + if (buffer_time > 0) { + err = snd_pcm_hw_params_set_buffer_time_near(handle, params, + &buffer_time, 0); + } else { + err = snd_pcm_hw_params_set_buffer_size_near(handle, params, + &buffer_frames); + } + assert(err >= 0); + monotonic = snd_pcm_hw_params_is_monotonic(params); + can_pause = snd_pcm_hw_params_can_pause(params); + err = snd_pcm_hw_params(handle, params); + if (err < 0) { + error(_("Unable to install hw params:")); + snd_pcm_hw_params_dump(params, log); + prg_exit(EXIT_FAILURE); + } + snd_pcm_hw_params_get_period_size(params, &chunk_size, 0); + snd_pcm_hw_params_get_buffer_size(params, &buffer_size); + if (chunk_size == buffer_size) { + error(_("Can't use period equal to buffer size (%lu == %lu)"), + chunk_size, buffer_size); + prg_exit(EXIT_FAILURE); + } + err = snd_pcm_sw_params_current(handle, swparams); + if (err < 0) { + error(_("Unable to get current sw params.")); + prg_exit(EXIT_FAILURE); + } + if (avail_min < 0) + n = chunk_size; + else + n = (double) rate * avail_min / 1000000; + err = snd_pcm_sw_params_set_avail_min(handle, swparams, n); + + /* round up to closest transfer boundary */ + n = buffer_size; + if (start_delay <= 0) { + start_threshold = n + (double) rate * start_delay / 1000000; + } else + start_threshold = (double) rate * start_delay / 1000000; + if (start_threshold < 1) + start_threshold = 1; + if (start_threshold > n) + start_threshold = n; + err = snd_pcm_sw_params_set_start_threshold(handle, swparams, start_threshold); + assert(err >= 0); + if (stop_delay <= 0) + stop_threshold = buffer_size + (double) rate * stop_delay / 1000000; + else + stop_threshold = (double) rate * stop_delay / 1000000; + err = snd_pcm_sw_params_set_stop_threshold(handle, swparams, stop_threshold); + assert(err >= 0); + + if (verbose) { + err = snd_pcm_sw_params_set_tstamp_mode(handle, swparams, SND_PCM_TSTAMP_ENABLE); + assert(err >= 0); + + if (monotonic) + err = snd_pcm_sw_params_set_tstamp_type(handle, swparams, SND_PCM_TSTAMP_TYPE_MONOTONIC); + else + err = snd_pcm_sw_params_set_tstamp_type(handle, swparams, SND_PCM_TSTAMP_TYPE_GETTIMEOFDAY); + assert(err >= 0); + } + + if (snd_pcm_sw_params(handle, swparams) < 0) { + error(_("unable to install sw params:")); + snd_pcm_sw_params_dump(swparams, log); + prg_exit(EXIT_FAILURE); + } + + if (setup_chmap()) + prg_exit(EXIT_FAILURE); + + if (verbose) + snd_pcm_dump(handle, log); + + bits_per_sample = snd_pcm_format_physical_width(hwparams.format); + significant_bits_per_sample = snd_pcm_format_width(hwparams.format); + bits_per_frame = bits_per_sample * hwparams.channels; + chunk_bytes = chunk_size * bits_per_frame / 8; + audiobuf = realloc(audiobuf, chunk_bytes); + if (audiobuf == NULL) { + error(_("not enough memory")); + prg_exit(EXIT_FAILURE); + } + // fprintf(stderr, "real chunk_size = %i, frags = %i, total = %i\n", chunk_size, setup.buf.block.frags, setup.buf.block.frags * chunk_size); + + /* stereo VU-meter isn't always available... */ + if (vumeter == VUMETER_STEREO) { + if (hwparams.channels != 2 || !interleaved || verbose > 2) + vumeter = VUMETER_MONO; + } + + /* show mmap buffer arragment */ + if (mmap_flag && verbose) { + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t offset, size = chunk_size; + int i; + err = snd_pcm_mmap_begin(handle, &areas, &offset, &size); + if (err < 0) { + error(_("snd_pcm_mmap_begin problem: %s"), snd_strerror(err)); + prg_exit(EXIT_FAILURE); + } + for (i = 0; i < (int)hwparams.channels; i++) + fprintf(stderr, "mmap_area[%i] = %p,%u,%u (%u)\n", i, areas[i].addr, areas[i].first, areas[i].step, snd_pcm_format_physical_width(hwparams.format)); + /* not required, but for sure */ + snd_pcm_mmap_commit(handle, offset, 0); + } + + buffer_frames = buffer_size; /* for position test */ +} + +static void init_stdin(void) +{ + struct termios term; + long flags; + + if (!interactive) + return; + if (!isatty(fileno(stdin))) { + interactive = 0; + return; + } + tcgetattr(fileno(stdin), &term); + term_c_lflag = term.c_lflag; + if (fd == fileno(stdin)) + return; + flags = fcntl(fileno(stdin), F_GETFL); + if (flags < 0 || fcntl(fileno(stdin), F_SETFL, flags|O_NONBLOCK) < 0) + fprintf(stderr, _("stdin O_NONBLOCK flag setup failed\n")); + term.c_lflag &= ~ICANON; + tcsetattr(fileno(stdin), TCSANOW, &term); +} + +static void done_stdin(void) +{ + struct termios term; + + if (!interactive) + return; + if (fd == fileno(stdin) || term_c_lflag == -1) + return; + tcgetattr(fileno(stdin), &term); + term.c_lflag = term_c_lflag; + tcsetattr(fileno(stdin), TCSANOW, &term); +} + +static char wait_for_input(void) +{ + struct pollfd pfd; + unsigned char b; + + do { + pfd.fd = fileno(stdin); + pfd.events = POLLIN; + poll(&pfd, 1, -1); + } while (read(fileno(stdin), &b, 1) != 1); + return b; +} + +static void do_pause(void) +{ + int err; + unsigned char b; + + if (!can_pause) { + fprintf(stderr, _("\rPAUSE command ignored (no hw support)\n")); + return; + } + if (snd_pcm_state(handle) == SND_PCM_STATE_SUSPENDED) + suspend(); + + err = snd_pcm_pause(handle, 1); + if (err < 0) { + error(_("pause push error: %s"), snd_strerror(err)); + return; + } + fprintf(stderr, _("\r=== PAUSE === ")); + fflush(stderr); + while (1) { + b = wait_for_input(); + if (b == ' ' || b == '\r') { + while (read(fileno(stdin), &b, 1) == 1); + if (snd_pcm_state(handle) == SND_PCM_STATE_SUSPENDED) + suspend(); + err = snd_pcm_pause(handle, 0); + if (err < 0) + error(_("pause release error: %s"), snd_strerror(err)); + return; + } + } +} + +static void check_stdin(void) +{ + unsigned char b; + + if (!interactive) + return; + if (fd != fileno(stdin)) { + while (read(fileno(stdin), &b, 1) == 1) { + if (b == ' ' || b == '\r') { + while (read(fileno(stdin), &b, 1) == 1); + do_pause(); + fprintf(stderr, " \r"); + fflush(stderr); + } + } + } +} + +#ifndef timersub +#define timersub(a, b, result) \ +do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \ + if ((result)->tv_usec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_usec += 1000000; \ + } \ +} while (0) +#endif + +#ifndef timermsub +#define timermsub(a, b, result) \ +do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \ + if ((result)->tv_nsec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_nsec += 1000000000L; \ + } \ +} while (0) +#endif + +/* I/O error handler */ +static void xrun(void) +{ + snd_pcm_status_t *status; + int res; + + snd_pcm_status_alloca(&status); + if ((res = snd_pcm_status(handle, status))<0) { + error(_("status error: %s"), snd_strerror(res)); + prg_exit(EXIT_FAILURE); + } + if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) { + if (monotonic) { +#ifdef HAVE_CLOCK_GETTIME + struct timespec now, diff, tstamp; + clock_gettime(CLOCK_MONOTONIC, &now); + snd_pcm_status_get_trigger_htstamp(status, &tstamp); + timermsub(&now, &tstamp, &diff); + fprintf(stderr, _("%s!!! (at least %.3f ms long)\n"), + stream == SND_PCM_STREAM_PLAYBACK ? _("underrun") : _("overrun"), + diff.tv_sec * 1000 + diff.tv_nsec / 1000000.0); +#else + fprintf(stderr, "%s !!!\n", _("underrun")); +#endif + } else { + struct timeval now, diff, tstamp; + gettimeofday(&now, 0); + snd_pcm_status_get_trigger_tstamp(status, &tstamp); + timersub(&now, &tstamp, &diff); + fprintf(stderr, _("%s!!! (at least %.3f ms long)\n"), + stream == SND_PCM_STREAM_PLAYBACK ? _("underrun") : _("overrun"), + diff.tv_sec * 1000 + diff.tv_usec / 1000.0); + } + if (verbose) { + fprintf(stderr, _("Status:\n")); + snd_pcm_status_dump(status, log); + } + if (fatal_errors) { + error(_("fatal %s: %s"), + stream == SND_PCM_STREAM_PLAYBACK ? _("underrun") : _("overrun"), + snd_strerror(res)); + prg_exit(EXIT_FAILURE); + } + if ((res = snd_pcm_prepare(handle))<0) { + error(_("xrun: prepare error: %s"), snd_strerror(res)); + prg_exit(EXIT_FAILURE); + } + return; /* ok, data should be accepted again */ + } + if (snd_pcm_status_get_state(status) == SND_PCM_STATE_DRAINING) { + if (verbose) { + fprintf(stderr, _("Status(DRAINING):\n")); + snd_pcm_status_dump(status, log); + } + if (stream == SND_PCM_STREAM_CAPTURE) { + fprintf(stderr, _("capture stream format change? attempting recover...\n")); + if ((res = snd_pcm_prepare(handle))<0) { + error(_("xrun(DRAINING): prepare error: %s"), snd_strerror(res)); + prg_exit(EXIT_FAILURE); + } + return; + } + } + if (verbose) { + fprintf(stderr, _("Status(R/W):\n")); + snd_pcm_status_dump(status, log); + } + error(_("read/write error, state = %s"), snd_pcm_state_name(snd_pcm_status_get_state(status))); + prg_exit(EXIT_FAILURE); +} + +/* I/O suspend handler */ +static void suspend(void) +{ + int res; + + if (!quiet_mode) { + fprintf(stderr, _("Suspended. Trying resume. ")); fflush(stderr); + } + while ((res = snd_pcm_resume(handle)) == -EAGAIN) + sleep(1); /* wait until suspend flag is released */ + if (res < 0) { + if (!quiet_mode) { + fprintf(stderr, _("Failed. Restarting stream. ")); fflush(stderr); + } + if ((res = snd_pcm_prepare(handle)) < 0) { + error(_("suspend: prepare error: %s"), snd_strerror(res)); + prg_exit(EXIT_FAILURE); + } + } + if (!quiet_mode) + fprintf(stderr, _("Done.\n")); +} + +static void print_vu_meter_mono(int perc, int maxperc) +{ + const int bar_length = 50; + char line[80]; + int val; + + for (val = 0; val <= perc * bar_length / 100 && val < bar_length; val++) + line[val] = '#'; + for (; val <= maxperc * bar_length / 100 && val < bar_length; val++) + line[val] = ' '; + line[val] = '+'; + for (++val; val <= bar_length; val++) + line[val] = ' '; + if (maxperc > 99) + sprintf(line + val, "| MAX"); + else + sprintf(line + val, "| %02i%%", maxperc); + fputs(line, stderr); + if (perc > 100) + fprintf(stderr, _(" !clip ")); +} + +static void print_vu_meter_stereo(int *perc, int *maxperc) +{ + const int bar_length = 35; + char line[80]; + int c; + + memset(line, ' ', sizeof(line) - 1); + line[bar_length + 3] = '|'; + + for (c = 0; c < 2; c++) { + int p = perc[c] * bar_length / 100; + char tmp[4]; + if (p > bar_length) + p = bar_length; + if (c) + memset(line + bar_length + 6 + 1, '#', p); + else + memset(line + bar_length - p, '#', p); + p = maxperc[c] * bar_length / 100 - 1; + if (p < 0) + p = 0; + else if (p >= bar_length) + p = bar_length - 1; + if (c) + line[bar_length + 6 + 1 + p] = '+'; + else + line[bar_length - p - 1] = '+'; + if (ABS(maxperc[c]) > 99) + sprintf(tmp, "MAX"); + else + sprintf(tmp, "%02d%%", maxperc[c]); + if (c) + memcpy(line + bar_length + 3 + 1, tmp, 3); + else + memcpy(line + bar_length, tmp, 3); + } + line[bar_length * 2 + 6 + 2] = 0; + fputs(line, stderr); +} + +static void print_vu_meter(signed int *perc, signed int *maxperc) +{ + if (vumeter == VUMETER_STEREO) + print_vu_meter_stereo(perc, maxperc); + else + print_vu_meter_mono(*perc, *maxperc); +} + +/* peak handler */ +static void compute_max_peak(uint8_t *data, size_t samples) +{ + signed int val, max, perc[2], max_peak[2]; + static int run = 0; + size_t osamples = samples; + int format_little_endian = snd_pcm_format_little_endian(hwparams.format); + int ichans, c; + + if (vumeter == VUMETER_STEREO) + ichans = 2; + else + ichans = 1; + + memset(max_peak, 0, sizeof(max_peak)); + switch (bits_per_sample) { + case 8: { + signed char *valp = (signed char *)data; + signed char mask = snd_pcm_format_silence(hwparams.format); + c = 0; + while (samples-- > 0) { + val = *valp++ ^ mask; + val = abs(val); + if (max_peak[c] < val) + max_peak[c] = val; + if (vumeter == VUMETER_STEREO) + c = !c; + } + break; + } + case 16: { + signed short *valp = (signed short *)data; + signed short mask = snd_pcm_format_silence_16(hwparams.format); + signed short sval; + + c = 0; + while (samples-- > 0) { + if (format_little_endian) + sval = le16toh(*valp); + else + sval = be16toh(*valp); + sval ^= mask; + val = abs(sval); + if (max_peak[c] < val) + max_peak[c] = val; + valp++; + if (vumeter == VUMETER_STEREO) + c = !c; + } + break; + } + case 24: { + unsigned char *valp = data; + signed int mask = snd_pcm_format_silence_32(hwparams.format); + + c = 0; + while (samples-- > 0) { + if (format_little_endian) { + val = valp[0] | (valp[1]<<8) | (valp[2]<<16); + } else { + val = (valp[0]<<16) | (valp[1]<<8) | valp[2]; + } + val ^= mask; + /* Correct signed bit in 32-bit value */ + if (val & (1<<(bits_per_sample-1))) { + val |= 0xff<<24; /* Negate upper bits too */ + } + val = abs(val); + if (max_peak[c] < val) + max_peak[c] = val; + valp += 3; + if (vumeter == VUMETER_STEREO) + c = !c; + } + break; + } + case 32: { + signed int *valp = (signed int *)data; + signed int mask = snd_pcm_format_silence_32(hwparams.format); + + c = 0; + while (samples-- > 0) { + if (format_little_endian) + val = le32toh(*valp); + else + val = be32toh(*valp); + val ^= mask; + if ((unsigned int)val == 0x80000000U) + val = 0x7fffffff; + else + val = abs(val); + if (max_peak[c] < val) + max_peak[c] = val; + valp++; + if (vumeter == VUMETER_STEREO) + c = !c; + } + break; + } + default: + if (run == 0) { + fprintf(stderr, _("Unsupported bit size %d.\n"), (int)bits_per_sample); + run = 1; + } + return; + } + max = 1 << (significant_bits_per_sample-1); + if (max <= 0) + max = 0x7fffffff; + + for (c = 0; c < ichans; c++) { + if (max_peak[c] > max) + max_peak[c] = max; + if (bits_per_sample > 16) + perc[c] = max_peak[c] / (max / 100); + else + perc[c] = max_peak[c] * 100 / max; + } + + if (interleaved && verbose <= 2) { + static int maxperc[2]; + static time_t t=0; + const time_t tt=time(NULL); + if(tt>t) { + t=tt; + maxperc[0] = 0; + maxperc[1] = 0; + } + for (c = 0; c < ichans; c++) + if (perc[c] > maxperc[c]) + maxperc[c] = perc[c]; + + putc('\r', stderr); + print_vu_meter(perc, maxperc); + fflush(stderr); + } + else if (verbose==3) { + fprintf(stderr, _("Max peak (%li samples): 0x%08x "), (long)osamples, max_peak[0]); + for (val = 0; val < 20; val++) + if (val <= perc[0] / 5) + putc('#', stderr); + else + putc(' ', stderr); + fprintf(stderr, " %i%%\n", perc[0]); + fflush(stderr); + } +} + +static void do_test_position(void) +{ + static long counter = 0; + static time_t tmr = -1; + time_t now; + static float availsum, delaysum, samples; + static snd_pcm_sframes_t maxavail, maxdelay; + static snd_pcm_sframes_t minavail, mindelay; + static snd_pcm_sframes_t badavail = 0, baddelay = 0; + snd_pcm_sframes_t outofrange; + snd_pcm_sframes_t avail, delay, savail, sdelay; + snd_pcm_status_t *status; + int err; + + snd_pcm_status_alloca(&status); + err = snd_pcm_avail_delay(handle, &avail, &delay); + if (err < 0) + return; + err = snd_pcm_status(handle, status); + if (err < 0) + return; + savail = snd_pcm_status_get_avail(status); + sdelay = snd_pcm_status_get_delay(status); + outofrange = (test_coef * (snd_pcm_sframes_t)buffer_frames) / 2; + if (avail > outofrange || avail < -outofrange || + delay > outofrange || delay < -outofrange) { + badavail = avail; baddelay = delay; + availsum = delaysum = samples = 0; + maxavail = maxdelay = 0; + minavail = mindelay = buffer_frames * 16; + fprintf(stderr, _("Suspicious buffer position (%li total): " + "avail = %li, delay = %li, buffer = %li\n"), + ++counter, (long)avail, (long)delay, (long)buffer_frames); + } else if (savail > outofrange || savail < -outofrange || + sdelay > outofrange || sdelay < -outofrange) { + badavail = savail; baddelay = sdelay; + availsum = delaysum = samples = 0; + maxavail = maxdelay = 0; + minavail = mindelay = buffer_frames * 16; + fprintf(stderr, _("Suspicious status buffer position (%li total): " + "avail = %li, delay = %li, buffer = %li\n"), + ++counter, (long)savail, (long)sdelay, (long)buffer_frames); + } else if (stream == SND_PCM_STREAM_CAPTURE && avail > delay) { + fprintf(stderr, _("Suspicious buffer position avail > delay (%li total): " + "avail = %li, delay = %li\n"), + ++counter, (long)avail, (long)delay); + } else if (stream == SND_PCM_STREAM_CAPTURE && savail > sdelay) { + fprintf(stderr, _("Suspicious status buffer position avail > delay (%li total): " + "avail = %li, delay = %li\n"), + ++counter, (long)savail, (long)sdelay); + } else if (verbose) { + time(&now); + if (tmr == (time_t) -1) { + tmr = now; + availsum = delaysum = samples = 0; + maxavail = maxdelay = 0; + minavail = mindelay = buffer_frames * 16; + } + if (avail > maxavail) + maxavail = avail; + if (savail > maxavail) + maxavail = savail; + if (delay > maxdelay) + maxdelay = delay; + if (sdelay > maxdelay) + maxdelay = sdelay; + if (avail < minavail) + minavail = avail; + if (savail < minavail) + minavail = savail; + if (delay < mindelay) + mindelay = delay; + if (sdelay < mindelay) + mindelay = sdelay; + availsum += avail; + delaysum += delay; + samples++; + if ((maxavail != 0 || maxdelay != 0) && now != tmr) { + fprintf(stderr, "BUFPOS: avg%li/%li " + "min%li/%li max%li/%li (%li) (%li:%li/%li)\n", + (long)(availsum / samples), + (long)(delaysum / samples), + (long)minavail, (long)mindelay, + (long)maxavail, (long)maxdelay, + (long)buffer_frames, + counter, badavail, baddelay); + tmr = now; + } + } + if (verbose == 1) { + fprintf(stderr, _("Status(R/W) (standalone avail=%li delay=%li):\n"), (long)avail, (long)delay); + snd_pcm_status_dump(status, log); + } +} + +/* + */ +#ifdef CONFIG_SUPPORT_CHMAP +static uint8_t *remap_data(uint8_t *data, size_t count) +{ + static uint8_t *tmp, *src, *dst; + static size_t tmp_size; + size_t sample_bytes = bits_per_sample / 8; + size_t step = bits_per_frame / 8; + size_t chunk_bytes; + unsigned int ch, i; + + if (!hw_map) + return data; + + chunk_bytes = count * bits_per_frame / 8; + if (tmp_size < chunk_bytes) { + free(tmp); + tmp = malloc(chunk_bytes); + if (!tmp) { + error(_("not enough memory")); + exit(1); + } + tmp_size = count; + } + + src = data; + dst = tmp; + for (i = 0; i < count; i++) { + for (ch = 0; ch < hwparams.channels; ch++) { + memcpy(dst, src + sample_bytes * hw_map[ch], + sample_bytes); + dst += sample_bytes; + } + src += step; + } + return tmp; +} + +static uint8_t **remap_datav(uint8_t **data, size_t count ATTRIBUTE_UNUSED) +{ + static uint8_t **tmp; + unsigned int ch; + + if (!hw_map) + return data; + + if (!tmp) { + tmp = malloc(sizeof(*tmp) * hwparams.channels); + if (!tmp) { + error(_("not enough memory")); + exit(1); + } + for (ch = 0; ch < hwparams.channels; ch++) + tmp[ch] = data[hw_map[ch]]; + } + return tmp; +} +#else +#define remap_data(data, count) (data) +#define remap_datav(data, count) (data) +#endif + +/* + * write function + */ + +static ssize_t pcm_write(uint8_t *data, size_t count) +{ + ssize_t r; + ssize_t result = 0; + + if (count < chunk_size) { + snd_pcm_format_set_silence(hwparams.format, data + count * bits_per_frame / 8, (chunk_size - count) * hwparams.channels); + count = chunk_size; + } + data = remap_data(data, count); + while (count > 0 && !in_aborting) { + if (test_position) + do_test_position(); + check_stdin(); + r = writei_func(handle, data, count); + if (test_position) + do_test_position(); + if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) { + if (!test_nowait) + snd_pcm_wait(handle, 100); + } else if (r == -EPIPE) { + xrun(); + } else if (r == -ESTRPIPE) { + suspend(); + } else if (r < 0) { + error(_("write error: %s"), snd_strerror(r)); + prg_exit(EXIT_FAILURE); + } + if (r > 0) { + if (vumeter) + compute_max_peak(data, r * hwparams.channels); + result += r; + count -= r; + data += r * bits_per_frame / 8; + } + } + return result; +} + +static ssize_t pcm_writev(uint8_t **data, unsigned int channels, size_t count) +{ + ssize_t r; + size_t result = 0; + + if (count != chunk_size) { + unsigned int channel; + size_t offset = count; + size_t remaining = chunk_size - count; + for (channel = 0; channel < channels; channel++) + snd_pcm_format_set_silence(hwparams.format, data[channel] + offset * bits_per_sample / 8, remaining); + count = chunk_size; + } + data = remap_datav(data, count); + while (count > 0 && !in_aborting) { + unsigned int channel; + void *bufs[channels]; + size_t offset = result; + for (channel = 0; channel < channels; channel++) + bufs[channel] = data[channel] + offset * bits_per_sample / 8; + if (test_position) + do_test_position(); + check_stdin(); + r = writen_func(handle, bufs, count); + if (test_position) + do_test_position(); + if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) { + if (!test_nowait) + snd_pcm_wait(handle, 100); + } else if (r == -EPIPE) { + xrun(); + } else if (r == -ESTRPIPE) { + suspend(); + } else if (r < 0) { + error(_("writev error: %s"), snd_strerror(r)); + prg_exit(EXIT_FAILURE); + } + if (r > 0) { + if (vumeter) { + for (channel = 0; channel < channels; channel++) + compute_max_peak(data[channel], r); + } + result += r; + count -= r; + } + } + return result; +} + +/* + * read function + */ + +static ssize_t pcm_read(uint8_t *data, size_t rcount) +{ + ssize_t r; + size_t result = 0; + size_t count = rcount; + + if (count != chunk_size) { + count = chunk_size; + } + + while (count > 0) { + if (in_aborting) + goto abort; + if (test_position) + do_test_position(); + check_stdin(); + r = readi_func(handle, data, count); + if (test_position) + do_test_position(); + if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) { + if (!test_nowait) + snd_pcm_wait(handle, 100); + } else if (r == -EPIPE) { + xrun(); + } else if (r == -ESTRPIPE) { + suspend(); + } else if (r < 0) { + error(_("read error: %s"), snd_strerror(r)); + prg_exit(EXIT_FAILURE); + } + if (r > 0) { + if (vumeter) + compute_max_peak(data, r * hwparams.channels); + result += r; + count -= r; + data += r * bits_per_frame / 8; + } + } +abort: + return result > rcount ? rcount : result; +} + +static ssize_t pcm_readv(uint8_t **data, unsigned int channels, size_t rcount) +{ + ssize_t r; + size_t result = 0; + size_t count = rcount; + + if (count != chunk_size) { + count = chunk_size; + } + + while (count > 0) { + if (in_aborting) + goto abort; + unsigned int channel; + void *bufs[channels]; + size_t offset = result; + for (channel = 0; channel < channels; channel++) + bufs[channel] = data[channel] + offset * bits_per_sample / 8; + if (test_position) + do_test_position(); + check_stdin(); + r = readn_func(handle, bufs, count); + if (test_position) + do_test_position(); + if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) { + if (!test_nowait) + snd_pcm_wait(handle, 100); + } else if (r == -EPIPE) { + xrun(); + } else if (r == -ESTRPIPE) { + suspend(); + } else if (r < 0) { + error(_("readv error: %s"), snd_strerror(r)); + prg_exit(EXIT_FAILURE); + } + if (r > 0) { + if (vumeter) { + for (channel = 0; channel < channels; channel++) + compute_max_peak(data[channel], r); + } + result += r; + count -= r; + } + } +abort: + return rcount; +} + +/* + * ok, let's play a .voc file + */ + +static ssize_t voc_pcm_write(uint8_t *data, size_t count) +{ + ssize_t result = count, r; + size_t size; + + while (count > 0 && !in_aborting) { + size = count; + if (size > chunk_bytes - buffer_pos) + size = chunk_bytes - buffer_pos; + memcpy(audiobuf + buffer_pos, data, size); + data += size; + count -= size; + buffer_pos += size; + if ((size_t)buffer_pos == chunk_bytes) { + if ((size_t)(r = pcm_write(audiobuf, chunk_size)) != chunk_size) + return r; + buffer_pos = 0; + } + } + return result; +} + +static void voc_write_silence(unsigned x) +{ + unsigned l; + uint8_t *buf; + + buf = (uint8_t *) malloc(chunk_bytes); + if (buf == NULL) { + error(_("can't allocate buffer for silence")); + return; /* not fatal error */ + } + snd_pcm_format_set_silence(hwparams.format, buf, chunk_size * hwparams.channels); + while (x > 0 && !in_aborting) { + l = x; + if (l > chunk_size) + l = chunk_size; + if (voc_pcm_write(buf, l) != (ssize_t)l) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + x -= l; + } + free(buf); +} + +static void voc_pcm_flush(void) +{ + if (buffer_pos > 0) { + size_t b; + if (snd_pcm_format_set_silence(hwparams.format, audiobuf + buffer_pos, chunk_bytes - buffer_pos * 8 / bits_per_sample) < 0) + fprintf(stderr, _("voc_pcm_flush - silence error")); + b = chunk_size; + if (pcm_write(audiobuf, b) != (ssize_t)b) + error(_("voc_pcm_flush error")); + } + snd_pcm_nonblock(handle, 0); + snd_pcm_drain(handle); + snd_pcm_nonblock(handle, nonblock); +} + +static void voc_play(int fd, int ofs, char *name) +{ + int l; + VocBlockType *bp; + VocVoiceData *vd; + VocExtBlock *eb; + size_t nextblock, in_buffer; + uint8_t *data, *buf; + char was_extended = 0, output = 0; + uint16_t *sp, repeat = 0; + off_t filepos = 0; + +#define COUNT(x) nextblock -= x; in_buffer -= x; data += x +#define COUNT1(x) in_buffer -= x; data += x + + data = buf = (uint8_t *)malloc(64 * 1024); + buffer_pos = 0; + if (data == NULL) { + error(_("malloc error")); + prg_exit(EXIT_FAILURE); + } + if (!quiet_mode) { + fprintf(stderr, _("Playing Creative Labs Channel file '%s'...\n"), name); + } + /* first we waste the rest of header, ugly but we don't need seek */ + while (ofs > (ssize_t)chunk_bytes) { + if ((size_t)safe_read(fd, buf, chunk_bytes) != chunk_bytes) { + error(_("read error")); + prg_exit(EXIT_FAILURE); + } + ofs -= chunk_bytes; + } + if (ofs) { + if (safe_read(fd, buf, ofs) != ofs) { + error(_("read error")); + prg_exit(EXIT_FAILURE); + } + } + hwparams.format = DEFAULT_FORMAT; + hwparams.subformat = SND_PCM_SUBFORMAT_STD; + hwparams.channels = 1; + hwparams.rate = DEFAULT_SPEED; + set_params(); + + in_buffer = nextblock = 0; + while (!in_aborting) { + Fill_the_buffer: /* need this for repeat */ + if (in_buffer < 32) { + /* move the rest of buffer to pos 0 and fill the buf up */ + if (in_buffer) + memcpy(buf, data, in_buffer); + data = buf; + if ((l = safe_read(fd, buf + in_buffer, chunk_bytes - in_buffer)) > 0) + in_buffer += l; + else if (!in_buffer) { + /* the file is truncated, so simulate 'Terminator' + and reduce the datablock for safe landing */ + nextblock = buf[0] = 0; + if (l == -1) { + perror(name); + prg_exit(EXIT_FAILURE); + } + } + } + while (!nextblock) { /* this is a new block */ + if (in_buffer < sizeof(VocBlockType)) + goto __end; + bp = (VocBlockType *) data; + COUNT1(sizeof(VocBlockType)); + nextblock = VOC_DATALEN(bp); + if (output && !quiet_mode) + fprintf(stderr, "\n"); /* write /n after ASCII-out */ + output = 0; + switch (bp->type) { + case 0: +#if 0 + d_printf("Terminator\n"); +#endif + return; /* VOC-file stop */ + case 1: + vd = (VocVoiceData *) data; + COUNT1(sizeof(VocVoiceData)); + /* we need a SYNC, before we can set new SPEED, STEREO ... */ + + if (!was_extended) { + hwparams.rate = (int) (vd->tc); + hwparams.rate = 1000000 / (256 - hwparams.rate); +#if 0 + d_printf("Channel data %d Hz\n", dsp_speed); +#endif + if (vd->pack) { /* /dev/dsp can't it */ + error(_("can't play packed .voc files")); + return; + } + if (hwparams.channels == 2) /* if we are in Stereo-Mode, switch back */ + hwparams.channels = 1; + } else { /* there was extended block */ + hwparams.channels = 2; + was_extended = 0; + } + set_params(); + break; + case 2: /* nothing to do, pure data */ +#if 0 + d_printf("Channel continuation\n"); +#endif + break; + case 3: /* a silence block, no data, only a count */ + sp = (uint16_t *) data; + COUNT1(sizeof(uint16_t)); + hwparams.rate = (int) (*data); + COUNT1(1); + hwparams.rate = 1000000 / (256 - hwparams.rate); + set_params(); +#if 0 + { + size_t silence; + silence = (((size_t) * sp) * 1000) / hwparams.rate; + d_printf("Silence for %d ms\n", (int) silence); + } +#endif + voc_write_silence(*sp); + break; + case 4: /* a marker for syncronisation, no effect */ + sp = (uint16_t *) data; + COUNT1(sizeof(uint16_t)); +#if 0 + d_printf("Marker %d\n", *sp); +#endif + break; + case 5: /* ASCII text, we copy to stderr */ + output = 1; +#if 0 + d_printf("ASCII - text :\n"); +#endif + break; + case 6: /* repeat marker, says repeatcount */ + /* my specs don't say it: maybe this can be recursive, but + I don't think somebody use it */ + repeat = *(uint16_t *) data; + COUNT1(sizeof(uint16_t)); +#if 0 + d_printf("Repeat loop %d times\n", repeat); +#endif + if (filepos >= 0) { /* if < 0, one seek fails, why test another */ + if ((filepos = lseek(fd, 0, 1)) < 0) { + error(_("can't play loops; %s isn't seekable\n"), name); + repeat = 0; + } else { + filepos -= in_buffer; /* set filepos after repeat */ + } + } else { + repeat = 0; + } + break; + case 7: /* ok, lets repeat that be rewinding tape */ + if (repeat) { + if (repeat != 0xFFFF) { +#if 0 + d_printf("Repeat loop %d\n", repeat); +#endif + --repeat; + } +#if 0 + else + d_printf("Neverending loop\n"); +#endif + lseek(fd, filepos, 0); + in_buffer = 0; /* clear the buffer */ + goto Fill_the_buffer; + } +#if 0 + else + d_printf("End repeat loop\n"); +#endif + break; + case 8: /* the extension to play Stereo, I have SB 1.0 :-( */ + was_extended = 1; + eb = (VocExtBlock *) data; + COUNT1(sizeof(VocExtBlock)); + hwparams.rate = (int) (eb->tc); + hwparams.rate = 256000000L / (65536 - hwparams.rate); + hwparams.channels = eb->mode == VOC_MODE_STEREO ? 2 : 1; + if (hwparams.channels == 2) + hwparams.rate = hwparams.rate >> 1; + if (eb->pack) { /* /dev/dsp can't it */ + error(_("can't play packed .voc files")); + return; + } +#if 0 + d_printf("Extended block %s %d Hz\n", + (eb->mode ? "Stereo" : "Mono"), dsp_speed); +#endif + break; + default: + error(_("unknown blocktype %d. terminate."), bp->type); + return; + } /* switch (bp->type) */ + } /* while (! nextblock) */ + /* put nextblock data bytes to dsp */ + l = in_buffer; + if (nextblock < (size_t)l) + l = nextblock; + if (l) { + if (output && !quiet_mode) { + if (xwrite(2, data, l) != l) { /* to stderr */ + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + } else { + if (voc_pcm_write(data, l) != l) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + } + COUNT(l); + } + } /* while(1) */ + __end: + if (!in_aborting) { + voc_pcm_flush(); + } + free(buf); +} +/* that was a big one, perhaps somebody split it :-) */ + +/* setting the globals for playing raw data */ +static void init_raw_data(void) +{ + hwparams = rhwparams; +} + +/* calculate the data count to read from/to dsp */ +static off_t calc_count(void) +{ + off_t count; + + if (timelimit == 0) + if (sampleslimit == 0) + count = pbrec_count; + else + count = snd_pcm_format_size(hwparams.format, sampleslimit * hwparams.channels); + else { + count = snd_pcm_format_size(hwparams.format, hwparams.rate * hwparams.channels); + count *= (off_t)timelimit; + } + return count < pbrec_count ? count : pbrec_count; +} + +/* write a .VOC-header */ +static void begin_voc(int fd, size_t cnt) +{ + VocHeader vh; + VocBlockType bt; + VocVoiceData vd; + VocExtBlock eb; + + memcpy(vh.magic, VOC_MAGIC_STRING, 20); + vh.headerlen = LE_SHORT(sizeof(VocHeader)); + vh.version = LE_SHORT(VOC_ACTUAL_VERSION); + vh.coded_ver = LE_SHORT(0x1233 - VOC_ACTUAL_VERSION); + + if (xwrite(fd, &vh, sizeof(VocHeader)) != sizeof(VocHeader)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + if (hwparams.channels > 1) { + /* write an extended block */ + bt.type = 8; + bt.datalen = 4; + bt.datalen_m = bt.datalen_h = 0; + if (xwrite(fd, &bt, sizeof(VocBlockType)) != sizeof(VocBlockType)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + eb.tc = LE_SHORT(65536 - 256000000L / (hwparams.rate << 1)); + eb.pack = 0; + eb.mode = 1; + if (xwrite(fd, &eb, sizeof(VocExtBlock)) != sizeof(VocExtBlock)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + } + bt.type = 1; + cnt += sizeof(VocVoiceData); /* Channel_data block follows */ + bt.datalen = (uint8_t) (cnt & 0xFF); + bt.datalen_m = (uint8_t) ((cnt & 0xFF00) >> 8); + bt.datalen_h = (uint8_t) ((cnt & 0xFF0000) >> 16); + if (xwrite(fd, &bt, sizeof(VocBlockType)) != sizeof(VocBlockType)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + vd.tc = (uint8_t) (256 - (1000000 / hwparams.rate)); + vd.pack = 0; + if (xwrite(fd, &vd, sizeof(VocVoiceData)) != sizeof(VocVoiceData)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } +} + +/* write a WAVE-header */ +static void begin_wave(int fd, size_t cnt) +{ + WaveHeader h; + WaveFmtBody f; + WaveChunkHeader cf, cd; + int bits; + uint32_t tmp; + uint16_t tmp2; + + /* WAVE cannot handle greater than 32bit (signed?) int */ + if (cnt == (size_t)-2) + cnt = 0x7fffff00; + + bits = 8; + switch ((unsigned long) hwparams.format) { + case SND_PCM_FORMAT_U8: + bits = 8; + break; + case SND_PCM_FORMAT_S16_LE: + bits = 16; + break; + case SND_PCM_FORMAT_S24_LE: /* S24_LE is 24 bits stored in 32 bit width with 8 bit padding */ + case SND_PCM_FORMAT_S32_LE: + case SND_PCM_FORMAT_FLOAT_LE: + bits = 32; + break; + case SND_PCM_FORMAT_S24_3LE: + bits = 24; + break; + default: + error(_("Wave doesn't support %s format..."), snd_pcm_format_name(hwparams.format)); + prg_exit(EXIT_FAILURE); + } + h.magic = WAV_RIFF; + tmp = cnt + sizeof(WaveHeader) + sizeof(WaveChunkHeader) + sizeof(WaveFmtBody) + sizeof(WaveChunkHeader) - 8; + h.length = LE_INT(tmp); + h.type = WAV_WAVE; + + cf.type = WAV_FMT; + cf.length = LE_INT(16); + + if (hwparams.format == SND_PCM_FORMAT_FLOAT_LE) + f.format = LE_SHORT(WAV_FMT_IEEE_FLOAT); + else + f.format = LE_SHORT(WAV_FMT_PCM); + f.channels = LE_SHORT(hwparams.channels); + f.sample_fq = LE_INT(hwparams.rate); +#if 0 + tmp2 = (samplesize == 8) ? 1 : 2; + f.byte_p_spl = LE_SHORT(tmp2); + tmp = dsp_speed * hwparams.channels * (uint32_t) tmp2; +#else + tmp2 = hwparams.channels * snd_pcm_format_physical_width(hwparams.format) / 8; + f.byte_p_spl = LE_SHORT(tmp2); + tmp = (uint32_t) tmp2 * hwparams.rate; +#endif + f.byte_p_sec = LE_INT(tmp); + f.bit_p_spl = LE_SHORT(bits); + + cd.type = WAV_DATA; + cd.length = LE_INT(cnt); + + if (xwrite(fd, &h, sizeof(WaveHeader)) != sizeof(WaveHeader) || + xwrite(fd, &cf, sizeof(WaveChunkHeader)) != sizeof(WaveChunkHeader) || + xwrite(fd, &f, sizeof(WaveFmtBody)) != sizeof(WaveFmtBody) || + xwrite(fd, &cd, sizeof(WaveChunkHeader)) != sizeof(WaveChunkHeader)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } +} + +/* write a Au-header */ +static void begin_au(int fd, size_t cnt) +{ + AuHeader ah; + + ah.magic = AU_MAGIC; + ah.hdr_size = BE_INT(24); + ah.data_size = BE_INT(cnt); + switch ((unsigned long) hwparams.format) { + case SND_PCM_FORMAT_MU_LAW: + ah.encoding = BE_INT(AU_FMT_ULAW); + break; + case SND_PCM_FORMAT_U8: + ah.encoding = BE_INT(AU_FMT_LIN8); + break; + case SND_PCM_FORMAT_S16_BE: + ah.encoding = BE_INT(AU_FMT_LIN16); + break; + default: + error(_("Sparc Audio doesn't support %s format..."), snd_pcm_format_name(hwparams.format)); + prg_exit(EXIT_FAILURE); + } + ah.sample_rate = BE_INT(hwparams.rate); + ah.channels = BE_INT(hwparams.channels); + if (xwrite(fd, &ah, sizeof(AuHeader)) != sizeof(AuHeader)) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } +} + +/* closing .VOC */ +static void end_voc(int fd) +{ + off_t length_seek; + VocBlockType bt; + size_t cnt; + char dummy = 0; /* Write a Terminator */ + + if (xwrite(fd, &dummy, 1) != 1) { + error(_("write error")); + prg_exit(EXIT_FAILURE); + } + length_seek = sizeof(VocHeader); + if (hwparams.channels > 1) + length_seek += sizeof(VocBlockType) + sizeof(VocExtBlock); + bt.type = 1; + cnt = fdcount; + cnt += sizeof(VocVoiceData); /* Channel_data block follows */ + if (cnt > 0x00ffffff) + cnt = 0x00ffffff; + bt.datalen = (uint8_t) (cnt & 0xFF); + bt.datalen_m = (uint8_t) ((cnt & 0xFF00) >> 8); + bt.datalen_h = (uint8_t) ((cnt & 0xFF0000) >> 16); + if (lseek(fd, length_seek, SEEK_SET) == length_seek) + xwrite(fd, &bt, sizeof(VocBlockType)); +} + +static void end_wave(int fd) +{ /* only close output */ + WaveChunkHeader cd; + off_t length_seek; + off_t filelen; + uint32_t rifflen; + + length_seek = sizeof(WaveHeader) + + sizeof(WaveChunkHeader) + + sizeof(WaveFmtBody); + cd.type = WAV_DATA; + cd.length = fdcount > 0x7fffffff ? LE_INT(0x7fffffff) : LE_INT(fdcount); + filelen = fdcount + 2*sizeof(WaveChunkHeader) + sizeof(WaveFmtBody) + 4; + rifflen = filelen > 0x7fffffff ? LE_INT(0x7fffffff) : LE_INT(filelen); + if (lseek(fd, 4, SEEK_SET) == 4) + xwrite(fd, &rifflen, 4); + if (lseek(fd, length_seek, SEEK_SET) == length_seek) + xwrite(fd, &cd, sizeof(WaveChunkHeader)); +} + +static void end_au(int fd) +{ /* only close output */ + AuHeader ah; + off_t length_seek; + + length_seek = (char *)&ah.data_size - (char *)&ah; + ah.data_size = fdcount > 0xffffffff ? 0xffffffff : BE_INT(fdcount); + if (lseek(fd, length_seek, SEEK_SET) == length_seek) + xwrite(fd, &ah.data_size, sizeof(ah.data_size)); +} + +static void header(int rtype, char *name) +{ + if (!quiet_mode) { + if (! name) + name = (stream == SND_PCM_STREAM_PLAYBACK) ? "stdout" : "stdin"; + fprintf(stderr, "%s %s '%s' : ", + (stream == SND_PCM_STREAM_PLAYBACK) ? _("Playing") : _("Recording"), + gettext(fmt_rec_table[rtype].what), + name); + fprintf(stderr, "%s, ", snd_pcm_format_description(hwparams.format)); + fprintf(stderr, _("Rate %d Hz, "), hwparams.rate); + if (hwparams.channels == 1) + fprintf(stderr, _("Mono")); + else if (hwparams.channels == 2) + fprintf(stderr, _("Stereo")); + else + fprintf(stderr, _("Channels %i"), hwparams.channels); + fprintf(stderr, "\n"); + } +} + +/* playing raw data */ + +static void playback_go(int fd, size_t loaded, off_t count, int rtype, char *name) +{ + int l, r; + off_t written = 0; + off_t c; + + header(rtype, name); + set_params(); + + while (loaded > chunk_bytes && written < count && !in_aborting) { + if (pcm_write(audiobuf + written, chunk_size) <= 0) + return; + written += chunk_bytes; + loaded -= chunk_bytes; + } + if (written > 0 && loaded > 0) + memmove(audiobuf, audiobuf + written, loaded); + + l = loaded; + while (written < count && !in_aborting) { + do { + c = count - written; + if (c > (off_t)chunk_bytes) + c = chunk_bytes; + + /* c < l, there is more data loaded + * then we actually need to write + */ + if (c < l) + l = c; + + c -= l; + + if (c == 0) + break; + r = safe_read(fd, audiobuf + l, c); + if (r < 0) { + perror(name); + prg_exit(EXIT_FAILURE); + } + fdcount += r; + if (r == 0) + break; + l += r; + } while ((size_t)l < chunk_bytes); + l = l * 8 / bits_per_frame; + r = pcm_write(audiobuf, l); + if (r != l) + break; + r = r * bits_per_frame / 8; + written += r; + l = 0; + } + if (!in_aborting) { + snd_pcm_nonblock(handle, 0); + snd_pcm_drain(handle); + snd_pcm_nonblock(handle, nonblock); + } +} + +static int read_header(int *loaded, int header_size) +{ + int ret; + struct stat buf; + + ret = fstat(fd, &buf); + if (ret < 0) { + perror("fstat"); + prg_exit(EXIT_FAILURE); + } + + /* don't be adventurous, get out if file size is smaller than + * requested header size */ + if ((buf.st_mode & S_IFMT) == S_IFREG && + buf.st_size < header_size) + return -1; + + if (*loaded < header_size) { + header_size -= *loaded; + ret = safe_read(fd, audiobuf + *loaded, header_size); + if (ret != header_size) { + error(_("read error")); + prg_exit(EXIT_FAILURE); + } + *loaded += header_size; + } + return 0; +} + +static int playback_au(char *name, int *loaded) +{ + if (read_header(loaded, sizeof(AuHeader)) < 0) + return -1; + + if (test_au(fd, audiobuf) < 0) + return -1; + + rhwparams.format = hwparams.format; + pbrec_count = calc_count(); + playback_go(fd, *loaded - sizeof(AuHeader), pbrec_count, FORMAT_AU, name); + + return 0; +} + +static int playback_voc(char *name, int *loaded) +{ + int ofs; + + if (read_header(loaded, sizeof(VocHeader)) < 0) + return -1; + + if ((ofs = test_vocfile(audiobuf)) < 0) + return -1; + + pbrec_count = calc_count(); + voc_play(fd, ofs, name); + + return 0; +} + +static int playback_wave(char *name, int *loaded) +{ + ssize_t dtawave; + + if (read_header(loaded, sizeof(WaveHeader)) < 0) + return -1; + + if ((dtawave = test_wavefile(fd, audiobuf, *loaded)) < 0) + return -1; + + pbrec_count = calc_count(); + playback_go(fd, dtawave, pbrec_count, FORMAT_WAVE, name); + + return 0; +} + +static int playback_raw(char *name, int *loaded) +{ + init_raw_data(); + pbrec_count = calc_count(); + playback_go(fd, *loaded, pbrec_count, FORMAT_RAW, name); + + return 0; +} + +/* + * let's play or capture it (capture_type says VOC/WAVE/raw) + */ + +static void playback(char *name) +{ + int loaded = 0; + + pbrec_count = LLONG_MAX; + fdcount = 0; + if (!name || !strcmp(name, "-")) { + fd = fileno(stdin); + name = "stdin"; + } else { + init_stdin(); + if ((fd = open(name, O_RDONLY, 0)) == -1) { + perror(name); + prg_exit(EXIT_FAILURE); + } + } + + switch(file_type) { + case FORMAT_AU: + playback_au(name, &loaded); + break; + case FORMAT_VOC: + playback_voc(name, &loaded); + break; + case FORMAT_WAVE: + playback_wave(name, &loaded); + break; + case FORMAT_RAW: + playback_raw(name, &loaded); + break; + default: + /* parse the file header */ + if (playback_au(name, &loaded) < 0 && + playback_voc(name, &loaded) < 0 && + playback_wave(name, &loaded) < 0) + playback_raw(name, &loaded); /* should be raw data */ + break; + } + + if (fd != fileno(stdin)) + close(fd); +} + +/** + * mystrftime + * + * Variant of strftime(3) that supports additional format + * specifiers in the format string. + * + * Parameters: + * + * s - destination string + * max - max number of bytes to write + * userformat - format string + * tm - time information + * filenumber - the number of the file, starting at 1 + * + * Returns: number of bytes written to the string s + */ +size_t mystrftime(char *s, size_t max, const char *userformat, + const struct tm *tm, const int filenumber) +{ + char formatstring[PATH_MAX] = ""; + char tempstring[PATH_MAX] = ""; + char *format, *tempstr; + const char *pos_userformat; + + format = formatstring; + + /* if mystrftime is called with userformat = NULL we return a zero length string */ + if (userformat == NULL) { + *s = '\0'; + return 0; + } + + for (pos_userformat = userformat; *pos_userformat; ++pos_userformat) { + if (*pos_userformat == '%') { + tempstr = tempstring; + tempstr[0] = '\0'; + switch (*++pos_userformat) { + + case '\0': // end of string + --pos_userformat; + break; + + case 'v': // file number + sprintf(tempstr, "%02d", filenumber); + break; + + default: // All other codes will be handled by strftime + *format++ = '%'; + *format++ = *pos_userformat; + continue; + } + + /* If a format specifier was found and used, copy the result. */ + if (tempstr[0]) { + while ((*format = *tempstr++) != '\0') + ++format; + continue; + } + } + + /* For any other character than % we simply copy the character */ + *format++ = *pos_userformat; + } + + *format = '\0'; + format = formatstring; + return strftime(s, max, format, tm); +} + +static int new_capture_file(char *name, char *namebuf, size_t namelen, + int filecount) +{ + char *s; + char buf[PATH_MAX-10]; + time_t t; + struct tm *tmp; + + if (use_strftime) { + t = time(NULL); + tmp = localtime(&t); + if (tmp == NULL) { + perror("localtime"); + prg_exit(EXIT_FAILURE); + } + if (mystrftime(namebuf, namelen, name, tmp, filecount+1) == 0) { + fprintf(stderr, "mystrftime returned 0"); + prg_exit(EXIT_FAILURE); + } + return filecount; + } + + /* get a copy of the original filename */ + strncpy(buf, name, sizeof(buf)); + buf[sizeof(buf)-1] = '\0'; + + /* separate extension from filename */ + s = buf + strlen(buf); + while (s > buf && *s != '.' && *s != '/') + --s; + if (*s == '.') + *s++ = 0; + else if (*s == '/') + s = buf + strlen(buf); + + /* upon first jump to this if block rename the first file */ + if (filecount == 1) { + if (*s) + snprintf(namebuf, namelen, "%s-01.%s", buf, s); + else + snprintf(namebuf, namelen, "%s-01", buf); + remove(namebuf); + rename(name, namebuf); + filecount = 2; + } + + /* name of the current file */ + if (*s) + snprintf(namebuf, namelen, "%s-%02i.%s", buf, filecount, s); + else + snprintf(namebuf, namelen, "%s-%02i", buf, filecount); + + return filecount; +} + +/** + * create_path + * + * This function creates a file path, like mkdir -p. + * + * Parameters: + * + * path - the path to create + * + * Returns: 0 on success, -1 on failure + * On failure, a message has been printed to stderr. + */ +int create_path(const char *path) +{ + char *start; + mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + + if (path[0] == '/') + start = strchr(path + 1, '/'); + else + start = strchr(path, '/'); + + while (start) { + char *buffer = strdup(path); + buffer[start-path] = 0x00; + + if (mkdir(buffer, mode) == -1 && errno != EEXIST) { + fprintf(stderr, "Problem creating directory %s", buffer); + perror(" "); + free(buffer); + return -1; + } + free(buffer); + start = strchr(start + 1, '/'); + } + return 0; +} + +static int safe_open(const char *name) +{ + int fd; + + fd = open(name, O_WRONLY | O_CREAT, 0644); + if (fd == -1) { + if (errno != ENOENT || !use_strftime) + return -1; + if (create_path(name) == 0) + fd = open(name, O_WRONLY | O_CREAT, 0644); + } + return fd; +} + +static void capture(char *orig_name) +{ + int tostdout=0; /* boolean which describes output stream */ + int filecount=0; /* number of files written */ + char *name = orig_name; /* current filename */ + char namebuf[PATH_MAX+2]; + off_t count, rest; /* number of bytes to capture */ + struct stat statbuf; + + /* setup sound hardware */ + set_params(); + + /* get number of bytes to capture */ + count = calc_count(); + if (count == 0) + count = LLONG_MAX; + /* compute the number of bytes per file */ + max_file_size = (long long) max_file_time * + snd_pcm_format_size(hwparams.format, + hwparams.rate * hwparams.channels); + /* WAVE-file should be even (I'm not sure), but wasting one byte + isn't a problem (this can only be in 8 bit mono) */ + if (count < LLONG_MAX) + count += count % 2; + else + count -= count % 2; + + /* display verbose output to console */ + header(file_type, name); + + /* write to stdout? */ + if (!name || !strcmp(name, "-")) { + fd = fileno(stdout); + name = "stdout"; + tostdout = 1; + if (count > fmt_rec_table[file_type].max_filesize) + count = fmt_rec_table[file_type].max_filesize; + } + init_stdin(); + + do { + /* open a file to write */ + if (!tostdout) { + /* upon the second file we start the numbering scheme */ + if (filecount || use_strftime) { + filecount = new_capture_file(orig_name, namebuf, + sizeof(namebuf), + filecount); + name = namebuf; + } + + /* open a new file */ + if (!lstat(name, &statbuf)) { + if (S_ISREG(statbuf.st_mode)) + remove(name); + } + fd = safe_open(name); + if (fd < 0) { + perror(name); + prg_exit(EXIT_FAILURE); + } + filecount++; + } + + rest = count; + if (rest > fmt_rec_table[file_type].max_filesize) + rest = fmt_rec_table[file_type].max_filesize; + if (max_file_size && (rest > max_file_size)) + rest = max_file_size; + + /* setup sample header */ + if (fmt_rec_table[file_type].start) + fmt_rec_table[file_type].start(fd, rest); + + /* capture */ + fdcount = 0; + while (rest > 0 && recycle_capture_file == 0 && !in_aborting) { + size_t c = (rest <= (off_t)chunk_bytes) ? + (size_t)rest : chunk_bytes; + size_t f = c * 8 / bits_per_frame; + size_t read = pcm_read(audiobuf, f); + size_t save; + if (read != f) + in_aborting = 1; + save = read * bits_per_frame / 8; + if (xwrite(fd, audiobuf, save) != (ssize_t)save) { + perror(name); + in_aborting = 1; + break; + } + count -= c; + rest -= c; + fdcount += save; + } + + /* re-enable SIGUSR1 signal */ + if (recycle_capture_file) { + recycle_capture_file = 0; + signal(SIGUSR1, signal_handler_recycle); + } + + /* finish sample container */ + if (!tostdout) { + if (fmt_rec_table[file_type].end) + fmt_rec_table[file_type].end(fd); + close(fd); + fd = -1; + } + + if (in_aborting) + prg_exit(EXIT_FAILURE); + + /* repeat the loop when format is raw without timelimit or + * requested counts of data are recorded + */ + } while ((file_type == FORMAT_RAW && !timelimit && !sampleslimit) || count > 0); +} + +static void playbackv_go(int* fds, unsigned int channels, size_t loaded, off_t count, int rtype, char **names) +{ + int r; + size_t vsize; + + unsigned int channel; + uint8_t *bufs[channels]; + + header(rtype, names[0]); + set_params(); + + vsize = chunk_bytes / channels; + + // Not yet implemented + assert(loaded == 0); + + for (channel = 0; channel < channels; ++channel) + bufs[channel] = audiobuf + vsize * channel; + + while (count > 0 && !in_aborting) { + size_t c = 0; + size_t expected = count / channels; + if (expected > vsize) + expected = vsize; + do { + r = safe_read(fds[0], bufs[0], expected); + if (r < 0) { + perror(names[0]); + prg_exit(EXIT_FAILURE); + } + for (channel = 1; channel < channels; ++channel) { + if (safe_read(fds[channel], bufs[channel], r) != r) { + perror(names[channel]); + prg_exit(EXIT_FAILURE); + } + } + if (r == 0) + break; + c += r; + } while (c < expected); + c = c * 8 / bits_per_sample; + r = pcm_writev(bufs, channels, c); + if ((size_t)r != c) + break; + r = r * bits_per_frame / 8; + count -= r; + } + if (!in_aborting) { + snd_pcm_nonblock(handle, 0); + snd_pcm_drain(handle); + snd_pcm_nonblock(handle, nonblock); + } +} + +static void capturev_go(int* fds, unsigned int channels, off_t count, int rtype, char **names) +{ + size_t c; + ssize_t r; + unsigned int channel; + size_t vsize; + uint8_t *bufs[channels]; + + header(rtype, names[0]); + set_params(); + + vsize = chunk_bytes / channels; + + for (channel = 0; channel < channels; ++channel) + bufs[channel] = audiobuf + vsize * channel; + + while (count > 0 && !in_aborting) { + size_t rv; + c = count; + if (c > chunk_bytes) + c = chunk_bytes; + c = c * 8 / bits_per_frame; + if ((size_t)(r = pcm_readv(bufs, channels, c)) != c) + break; + rv = r * bits_per_sample / 8; + for (channel = 0; channel < channels; ++channel) { + if ((size_t)xwrite(fds[channel], bufs[channel], rv) != rv) { + perror(names[channel]); + prg_exit(EXIT_FAILURE); + } + } + r = r * bits_per_frame / 8; + count -= r; + fdcount += r; + } +} + +static void playbackv(char **names, unsigned int count) +{ + int ret = 0; + unsigned int channel; + unsigned int channels = rhwparams.channels; + int alloced = 0; + int fds[channels]; + for (channel = 0; channel < channels; ++channel) + fds[channel] = -1; + + if (count == 1 && channels > 1) { + size_t len = strlen(names[0]); + char buf[len + 1]; + strcpy(buf, names[0]); + /* 1 for "." + 3 for channel (<= 256) + 1 for null terminator */ + len += 5; + names = malloc(sizeof(*names) * channels); + for (channel = 0; channel < channels; ++channel) { + names[channel] = malloc(len); + snprintf(names[channel], len, "%s.%d", buf, channel); + } + alloced = 1; + } else if (count != channels) { + error(_("You need to specify %u files"), channels); + prg_exit(EXIT_FAILURE); + } + + for (channel = 0; channel < channels; ++channel) { + fds[channel] = open(names[channel], O_RDONLY, 0); + if (fds[channel] < 0) { + perror(names[channel]); + ret = EXIT_FAILURE; + goto __end; + } + } + /* should be raw data */ + init_raw_data(); + pbrec_count = calc_count(); + playbackv_go(fds, channels, 0, pbrec_count, FORMAT_RAW, names); + + __end: + for (channel = 0; channel < channels; ++channel) { + if (fds[channel] >= 0) + close(fds[channel]); + if (alloced) + free(names[channel]); + } + if (alloced) + free(names); + if (ret) + prg_exit(ret); +} + +static void capturev(char **names, unsigned int count) +{ + int ret = 0; + unsigned int channel; + unsigned int channels = rhwparams.channels; + int alloced = 0; + int fds[channels]; + for (channel = 0; channel < channels; ++channel) + fds[channel] = -1; + + if (count == 1) { + size_t len = strlen(names[0]); + char buf[len + 1]; + strcpy(buf, names[0]); + /* 1 for "." + 3 for channel (<= 256) + 1 for null terminator */ + len += 5; + names = malloc(sizeof(*names) * channels); + for (channel = 0; channel < channels; ++channel) { + names[channel] = malloc(len); + snprintf(names[channel], len, "%s.%d", buf, channel); + } + alloced = 1; + } else if (count != channels) { + error(_("You need to specify %d files"), channels); + prg_exit(EXIT_FAILURE); + } + + for (channel = 0; channel < channels; ++channel) { + fds[channel] = open(names[channel], O_WRONLY + O_CREAT, 0644); + if (fds[channel] < 0) { + perror(names[channel]); + ret = EXIT_FAILURE; + goto __end; + } + } + /* should be raw data */ + init_raw_data(); + pbrec_count = calc_count(); + capturev_go(fds, channels, pbrec_count, FORMAT_RAW, names); + + __end: + for (channel = 0; channel < channels; ++channel) { + if (fds[channel] >= 0) + close(fds[channel]); + if (alloced) + free(names[channel]); + } + if (alloced) + free(names); + if (ret) + prg_exit(ret); +} diff --git a/aplay/formats.h b/aplay/formats.h new file mode 100644 index 0000000..d82505e --- /dev/null +++ b/aplay/formats.h @@ -0,0 +1,133 @@ +#ifndef FORMATS_H +#define FORMATS_H 1 + +#include "bswap.h" + +/* Definitions for .VOC files */ + +#define VOC_MAGIC_STRING "Creative Voice File\x1A" +#define VOC_ACTUAL_VERSION 0x010A +#define VOC_SAMPLESIZE 8 + +#define VOC_MODE_MONO 0 +#define VOC_MODE_STEREO 1 + +#define VOC_DATALEN(bp) ((u_long)(bp->datalen) | \ + ((u_long)(bp->datalen_m) << 8) | \ + ((u_long)(bp->datalen_h) << 16) ) + +typedef struct voc_header { + uint8_t magic[20]; /* must be MAGIC_STRING */ + uint16_t headerlen; /* Headerlength, should be 0x1A */ + uint16_t version; /* VOC-file version */ + uint16_t coded_ver; /* 0x1233-version */ +} VocHeader; + +typedef struct voc_blocktype { + uint8_t type; + uint8_t datalen; /* low-byte */ + uint8_t datalen_m; /* medium-byte */ + uint8_t datalen_h; /* high-byte */ +} VocBlockType; + +typedef struct voc_voice_data { + uint8_t tc; + uint8_t pack; +} VocVoiceData; + +typedef struct voc_ext_block { + uint16_t tc; + uint8_t pack; + uint8_t mode; +} VocExtBlock; + +/* Definitions for Microsoft WAVE format */ + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24)) +#define LE_SHORT(v) (v) +#define LE_INT(v) (v) +#define BE_SHORT(v) bswap_16(v) +#define BE_INT(v) bswap_32(v) +#elif __BYTE_ORDER == __BIG_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((d) | ((c)<<8) | ((b)<<16) | ((a)<<24)) +#define LE_SHORT(v) bswap_16(v) +#define LE_INT(v) bswap_32(v) +#define BE_SHORT(v) (v) +#define BE_INT(v) (v) +#else +#error "Wrong endian" +#endif + +/* Note: the following macros evaluate the parameter v twice */ +#define TO_CPU_SHORT(v, be) \ + ((be) ? BE_SHORT(v) : LE_SHORT(v)) +#define TO_CPU_INT(v, be) \ + ((be) ? BE_INT(v) : LE_INT(v)) + +#define WAV_RIFF COMPOSE_ID('R','I','F','F') +#define WAV_RIFX COMPOSE_ID('R','I','F','X') +#define WAV_WAVE COMPOSE_ID('W','A','V','E') +#define WAV_FMT COMPOSE_ID('f','m','t',' ') +#define WAV_DATA COMPOSE_ID('d','a','t','a') + +/* WAVE fmt block constants from Microsoft mmreg.h header */ +#define WAV_FMT_PCM 0x0001 +#define WAV_FMT_IEEE_FLOAT 0x0003 +#define WAV_FMT_DOLBY_AC3_SPDIF 0x0092 +#define WAV_FMT_EXTENSIBLE 0xfffe + +/* Used with WAV_FMT_EXTENSIBLE format */ +#define WAV_GUID_TAG "\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71" + +/* it's in chunks like .voc and AMIGA iff, but my source say there + are in only in this combination, so I combined them in one header; + it works on all WAVE-file I have + */ +typedef struct { + uint32_t magic; /* 'RIFF' */ + uint32_t length; /* filelen */ + uint32_t type; /* 'WAVE' */ +} WaveHeader; + +typedef struct { + uint16_t format; /* see WAV_FMT_* */ + uint16_t channels; + uint32_t sample_fq; /* frequence of sample */ + uint32_t byte_p_sec; + uint16_t byte_p_spl; /* samplesize; 1 or 2 bytes */ + uint16_t bit_p_spl; /* 8, 12 or 16 bit */ +} WaveFmtBody; + +typedef struct { + WaveFmtBody format; + uint16_t ext_size; + uint16_t bit_p_spl; + uint32_t channel_mask; + uint16_t guid_format; /* WAV_FMT_* */ + uint8_t guid_tag[14]; /* WAV_GUID_TAG */ +} WaveFmtExtensibleBody; + +typedef struct { + uint32_t type; /* 'data' */ + uint32_t length; /* samplecount */ +} WaveChunkHeader; + +/* Definitions for Sparc .au header */ + +#define AU_MAGIC COMPOSE_ID('.','s','n','d') + +#define AU_FMT_ULAW 1 +#define AU_FMT_LIN8 2 +#define AU_FMT_LIN16 3 + +typedef struct au_header { + uint32_t magic; /* '.snd' */ + uint32_t hdr_size; /* size of header (min 24) */ + uint32_t data_size; /* size of data */ + uint32_t encoding; /* see to AU_FMT_XXXX */ + uint32_t sample_rate; /* sample rate */ + uint32_t channels; /* number of channels (voices) */ +} AuHeader; + +#endif /* FORMATS */ diff --git a/axfer/Makefile.am b/axfer/Makefile.am new file mode 100644 index 0000000..a00176c --- /dev/null +++ b/axfer/Makefile.am @@ -0,0 +1,72 @@ +bin_PROGRAMS = \ + axfer + +man_MANS = \ + axfer.1 \ + axfer-list.1 \ + axfer-transfer.1 + +# To include headers for gettext and version. +AM_CPPFLAGS = \ + -I$(top_srcdir)/include + +# Unit tests. +SUBDIRS = \ + test + +LIBRT = @LIBRT@ +LDADD = \ + $(LIBINTL) \ + $(LIBRT) + +noinst_HEADERS = \ + misc.h \ + subcmd.h \ + container.h \ + mapper.h \ + xfer.h \ + xfer-libasound.h \ + frame-cache.h + waiter.h + +axfer_SOURCES = \ + misc.h \ + subcmd.h \ + main.c \ + subcmd-list.c \ + container.h \ + container.c \ + container-riff-wave.c \ + container-au.c \ + container-voc.c \ + container-raw.c \ + mapper.h \ + mapper.c \ + mapper-single.c \ + mapper-multiple.c \ + xfer.h \ + xfer.c \ + xfer-options.c \ + xfer-libasound.h \ + xfer-libasound.c \ + frame-cache.h \ + frame-cache.c \ + xfer-libasound-irq-rw.c \ + subcmd-transfer.c \ + xfer-libasound-irq-mmap.c \ + waiter.h \ + waiter.c \ + waiter-poll.c \ + waiter-select.c \ + waiter-epoll.c \ + xfer-libasound-timer-mmap.c + +if HAVE_FFADO +axfer_SOURCES += xfer-libffado.c +LDADD += -lffado +endif + +EXTRA_DIST = \ + axfer.1 \ + axfer-list.1 \ + axfer-transfer.1 diff --git a/axfer/axfer-list.1 b/axfer/axfer-list.1 new file mode 100644 index 0000000..306d928 --- /dev/null +++ b/axfer/axfer-list.1 @@ -0,0 +1,86 @@ +.TH AXFER\-LIST 1 "28 November 2018" "alsa\-utils" + +.SH NAME +axfer\-list \- dump lists of available sound devices and nodes to transfer audio +data frame. + +.SH SYNOPSIS + +.B axfer list +.I direction target + +direction = +.B capture +| +.B playback + +target = +.B device +| +.B pcm + +.SH DESCRIPTION +The +.B list +subcommand of +.B axfer +dumps lists of available nodes to +transfer audio data frame. At present, the subcommand is helpful just for +libasound backend of +.B transfer +subcommand. + +.SH OPTIONS + +.SS Direction + +.TP +.B capture +Operates for capture transmission. + +.TP +.B playback +Operates for playback transmission. + +.SS Target + +.TP +.B device +Dumps a list of all soundcards and digital audio devices available in +.I libasound +backend for +.I tranfer +subcommand. + +.TP +.B pcm +Dumps a list of all PCM nodes available in alsa\-lib configuration space in +.I libasound +backend for +.I transfer +subcommand. + +.SH COMPATIBILITY TO APLAY + +Options of +.I \-l +, +.I \-\-list-devices +are handled as +.I device +operation. Options of +.I \-L +, +.I \-\-list-pcms +are handled as +.I pcm +operation. + +.SH SEE ALSO +.B axfer(1), +.B axfer\-transfer(1), +.B alsamixer(1), +.B amixer(1) + +.SH AUTHOR +Takashi Sakamoto diff --git a/axfer/axfer-transfer.1 b/axfer/axfer-transfer.1 new file mode 100644 index 0000000..2e831b2 --- /dev/null +++ b/axfer/axfer-transfer.1 @@ -0,0 +1,1009 @@ +.TH AXFER\-TRANSFER 1 "28 November 2018" "alsa\-utils" + +.SH NAME +axfer\-transfer \- transferrer of audio data frame for sound devices and nodes. + +.SH SYNOPSIS + +.B axfer transfer +.I direction +[ +.I common\-options +] [ +.I backend\-options +] [ +.I filepath +] + +.B axfer transfer +.I direction +[ +.I common\-options +] [ +.I backend\-options +] +.I \-I +| +.I \-\-separate\-channels filepath ... + +direction = +.B capture +| +.B playback + +common\-options = ( read +.I OPTIONS +section ) + +backend\-options = ( read +.I OPTIONS +section ) + +filepaths = ( read +.I OPTIONS +section ) + +.SH DESCRIPTION +The +.B transfer +subcommand of +.B axfer +performs transmission of audio data frames for devices available in supported +backends. This program is essentially designed to use alsa\-lib APIs +(libasound backend) to handle sound devices supported by Linux sound subsystem +(ALSA). + +.SH OPTIONS + +.SS Direction + +.TP +.B capture +Operates for capture transmission. + +.TP +.B playback +Operates for playback transmission. + +.SS Filepath + +Filepath is handled as a path relative to current working directory of run time +if it\(aqs not full path from root directory. + +The standard input or output is used if filepath is not specified or given as +.I \(aq\-\(aq +\&. + +For playback transmission, container format of given +.I filepath +is detected automatically and metadata is used for parameters of sample format, +channels, rate, duration. If nothing detected, content of given file path is +handled as raw data. In this case, the parameters should be indicated as +options. + +Multiple +.I filepaths +are allowed with +.I \-I +| +.I \-\-separate\-channels +option. In this case, standard input and output is not available. The same +.I filepath +is not allowed except for paths listed below: + \- /dev/null + \- /dev/zero + \- /dev/full + \- /dev/random + \- /dev/urandom + +.SS Common options + +.TP +.B \-h, \-\-help +Print help messages and finish run time. + +.TP +.B \-q, \-\-quiet +Quiet mode. Suppress messages (not sound :)) + +.TP +.B \-v, \-\-verbose +Verbose mode. Runtime dumps supplemental information according to the number of +this option given in command line. + +.TP +.B \-d, \-\-duration=# +Interrupt after # seconds. A value of zero means infinity. The default is zero, +so if this option is omitted then the transmission process will run until it is +killed. Either +.I \-d +or +.I \-s +option is available exclusively. + +.TP +.B \-s, \-\-samples=# +Interrupt after transmission of # number of data frames. A value of zero means +infinity. The default is zero, so if this options is omitted then the +transmission process will run until it is killed. Either +.I \-d +or +.I \-s +option is available exclusively. + +.TP +.B \-f, \-\-format=FORMAT +Indicate format of audio sample. This is required for capture transmission, or +playback transmission with files including raw audio data. + +Available sample format is listed below: + - [S8|U8|S16|U16|S32|U32][_LE|_BE] + - [S24|U24][_LE|_BE] + - FLOAT[_LE|_BE] + - FLOAT64[_LE|_BE] + - IEC958_SUBFRAME[_LE|_BE] + - MU_LAW + - A_LAW + - [S20|U20][_LE|_BE] + - [S24|U24][_3LE|_3BE] + - [S20|U20][_3LE|_3BE] + - [S18|U18][_3LE|_3BE] + - DSD_U8 + - DSD_[U16|U32][_LE|_BE] + +If endian\-ness is omitted, host endian\-ness is used. + +Some special formats are available: + - cd (16 bit little endian, 44100, stereo) [= \-f S16_LE \-c 2 \-r 44100] + - cdr (16 bit big endian, 44100, stereo) [= \-f S16_BE \-c 2 \-f 44100] + - dat (16 bit little endian, 48000, stereo) [= \-f S16_LE \-c 2 \-r 48000] + +If omitted, +.I U8 +is used as a default. Actual available formats are restricted by each +transmission backend. + +Unavailable sample format is listed below. These format has size of data frame +unaligned to byte unit. + + - IMA_ADPCM + - MPEG + - GSM + - SPECIAL + - G723_24 + - G723_24_1B + - G723_40 + - G723_40_1B + +.TP +.B \-c, \-\-channels=# +Indicate the number of audio data samples per frame. This is required for +capture transmission, or playback transmission with files including raw audio +data. The value should be between +.I 1 to +.I 256 +\&. If omitted, +.I 1 +is used as a default. + +.TP +.B \-r, \-\-rate=# +Indicate the number of audio data frame per second. This is required for +capture transmission, or playback transmission with files including raw audio +data. If the value is less than +.I 1000 +, it\(aqs interpreted by +.I kHz +unit. The value should be between +.I 2000 +and +.I 192000 +\&. If omitted, +.I 8000 +is used as a default. + +.TP +.B \-t, \-\-file\-type=TYPE +Indicate the type of file. This is required for capture transmission. Available +types are listed below: + - wav: Microsoft/IBM RIFF/Wave format + - au, sparc: Sparc AU format + - voc: Creative Tech. voice format + - raw: raw data + +When nothing is indicated, for capture transmission, the type is decided +according to suffix of +.I filepath +, and +.I raw +type is used for fallback. + +.TP +.B \-I, \-\-separate\-channels +Indicate this option when several files are going to be handled. For capture +transmission, if one filepath is given as +.I filepath +, a list of +.I filepaths +is generated in a formula \(aq\-[.suffix]\(aq. +The suffix is omitted when raw format of container is used. + +.TP +.B \-\-dump\-hw\-params +Dump hardware parameters and finish run time if backend supports it. + +.TP +.B \-\-xfer\-backend=BACKEND +Select backend of transmission from a list below. The default is libasound. +.br + - libasound + - libffado (optional if compiled) + +.SS Backend options for libasound + +.TP +.B \-D, \-\-device=NODE + +This option is used to select PCM node in libasound configuration space. +Available nodes are listed by +.I pcm +operation of +.I list +subcommand. + +.TP +.B \-N, \-\-nonblock + +With this option, PCM substream is opened in non\-blocking mode. When audio +data frame is not available in buffer of the PCM substream, I/O operation +immediately returns without blocking process. This option implicitly uses +.I \-\-waiter\-type +option as well to prevent heavy consumption of CPU time. + +.TP +.B \-M, \-\-mmap + +With this option, audio data frame is processed directly in buffer of PCM +substream if selected node supports this operation. Without the option, +temporary buffers are used to copy audio data frame for buffer of PCM substream. +This option implicitly uses +.I \-\-waiter\-type +option as well to prevent heavy consumption of CPU time. + +.TP +.B \-F, \-\-period\-size=# + +This option configures given value to +.I period_size +hardware parameter of PCM substream. The parameter indicates the number of audio +data frame per period in buffer of the PCM substream. Actual number is decided +as a result of interaction between each implementation of PCM plugin chained +from the selected PCM node, and in\-kernel driver or PCM I/O plugins. + +Ideally, the same amount of audio data frame as the value should be handled in +one I/O operation. Actually, it is not, depending on implementation of the PCM +plugins, in\-kernel driver, PCM I/O plugins and scheduling model. For \(aqhw\(aq +PCM plugin in \(aqirq\(aq scheduling model, the value is used to decide +intervals of hardware interrupt, thus the same amount of audio data frame as +the value is expected to be available for one I/O operation. + +.TP +.B \-\-period\-time=# + +This option configures given value to +.I period_time +hardware parameter of PCM substream. This option is similar to +.I \-\-period\-size +option, however its unit is micro\-second. + +.TP +.B \-B, \-\-buffer\-size=# + +This option configures given value to +.I buffer_size +hardware parameter of PCM substream. The parameter indicates the number of audio +data frame in buffer of PCM substream. Actual number is decided as a result of +interaction between each implementation of PCM plugin chained from the selected +PCM node, and in\-kernel driver or PCM I/O plugins. + +Ideally, this is multiples of the number of audio data frame per period, thus +the size of period. Actually, it is not, depending on implementation of the PCM +plugins, in\-kernel driver and PCM I/O plugins. + +.TP +.B \-\-buffer\-time=# + +This option configures given value to +.I buffer_time +hardware parameter of PCM substream. This option is similar to +.I \-\-buffer\-size +option, however its unit is micro\-second. + +.TP +.B \-\-waiter\-type=TYPE + +This option indicates the type of waiter for event notification. At present, +four types are available; +.I default +, +.I select +, +.I poll +and +.I epoll +\&. With +.I default +type, \(aqsnd_pcm_wait()\(aq is used. With +.I select +type, \(aqselect(2)\(aq system call is used. With +.I poll +type, \(aqpoll(2)\(aq system call is used. With +.I epoll +type, Linux\-specific \(aqepoll(7)\(aq system call is used. + +This option should correspond to one of +.I \-\-nonblock +or +.I \-\-mmap +options, or +.I timer +value of +.I \-\-sched\-model +option. +Neither this option nor +.I \-\-test\-nowait +is available at the same time. + +.TP +.B \-\-sched\-model=MODEL + +This option selects scheduling model for process of this program. One of +.I irq +or +.I timer +is available. In detail, please read \(aqSCHEDULING MODEL\(aq section. + +When nothing specified, +.I irq +model is used. + +.TP +.B \-A, \-\-avail\-min=# + +This option configures given value to +.I avail\-min +software parameter of PCM substream. In blocking mode, the value is used as +threshold of the number of available audio data frames in buffer of PCM +substream to wake up process blocked by I/O operation. In non\-blocking mode, +any I/O operation returns \-EAGAIN until the available number of audio data frame reaches the threshold. + +This option has an effect in cases neither +.I \-\-mmap +nor +.I timer +value of +.I \-\-sched\-model +option is used. + +.TP +.B \-R, \-\-start\-delay=# + +This option configures given value to +.I start_threshold +software parameter of PCM substream. The value is used as threshold to start +PCM substream automatically. At present, this option has an effect in cases +neither +.I \-\-mmap +nor +.I timer +value of +.I \-\-sched\-model +option is used. + +For playback transmission, when the number of accumulated audio data frame +in buffer of PCM substream to which this program writes out reaches the +threshold, the PCM substream starts automatically without an explicit call of +.I snd_pcm_start() +to the PCM substream. + +For capture transmission, this option is useless. The number of +accumulated audio data frame is not increased without an explicit call of +.I snd_pcm_start() +to the PCM substream. + +This option has an effect in cases neither +.I \-\-mmap +nor +.I timer +value of +.I \-\-sched\-model +option is used. + +.TP +.B \-T, \-\-stop\-delay=# + +This option configures given value to +.I stop_threshold +software parameter of PCM substream. The value is used as threshold to stop PCM +substream automatically. At present, this option has an effect in cases neither +.I \-\-mmap +nor +.I timer +value of +.I \-\-sched\-model +option is used. + +For capture transmission, when the number of accumulated audio data frame +in buffer of PCM substream to which a driver or alsa\-lib PCM plugins write +reaches the threshold, the PCM substream stops automatically without an explicit +call of +.I snd_pcm_stop() +to the PCM substream. This is a case that this program leaves the audio data +frames without reading for a while. + +For playback transmission, when the number available audio data frame in buffer +of PCM substream from which a driver or alsa\-lib PCM plugins read reaches the +threshold, the PCM substream stops automatically without an explicit call of +.I snd_pcm_stop() +to the PCM substream. This is a case that this program leaves the audio data +frames without writing for a while. + +This option has an effect in cases neither +.I \-\-mmap +nor +.I timer +value of +.I \-\-sched\-model +option is used. + +.TP +.B \-\-disable\-resample + +This option has an effect for \(aqplug\(aq plugin in alsa\-lib to suppress +conversion of sampling rate for audio data frame. + +.TP +.B \-\-disable\-channels + +This option has an effect for \(aqplug\(aq plugin in alsa\-lib to suppress +conversion of channels for audio data frame. + +.TP +.B \-\-disable\-format + +This option has an effect for \(aqplug\(aq plugin in alsa\-lib to suppress +conversion of sample format for audio data frame. + +.TP +.B \-\-disable\-softvol + +This option has an effect for \(aqsoftvol\(aq plugin in alsa\-lib to suppress +conversion of samples for audio data frame via additional control element. + +.TP +.B \-\-fatal\-errors + +This option suppresses recovery operation from XRUN state of running PCM +substream, then process of this program is going to finish as usual. + +.TP +.B \-\-test\-nowait + +This option disables any waiter for I/O event notification. I/O operations are +iterated till any of audio data frame is available. The option brings heavy +load in consumption of CPU time. + +.SS Backend options for libffado + +This backend is automatically available when configure script detects +.I ffado_streaming_init() +symbol in libffado shared object. + +.TP +.B \-p, \-\-port=# + +This option uses given value to decide which 1394 OHCI controller is used to +communicate. When Linux system has two 1394 OHCI controllers, +.I 0 +or +.I 1 +are available. Neither this option nor +.I \-g +is available at the same time. If nothing specified, libffado performs to +communicate to units on IEEE 1394 bus managed by all of 1394 OHCI controller available in Linux system. + +.TP +.B \-n, \-\-node=# + +This option uses given value to decide which unit is used to communicate. This +option requires +.I \-p +option to indicate which 1394 OHCI controller is used to communicate to the +specified unit. + +.TP +.B \-g, \-\-guid=HEXADECIMAL + +This option uses given value to decide a target unit to communicate. The value +should be prefixed with '0x' and consists of hexadecimal literal letters +(0\-9, a\-f, A\-F). Neither this option nor +.I \-p +is available at the same time. If nothing specified, libffado performs to +communicate to units on IEEE 1394 bus managed by all of 1394 OHCI controller +available in Linux system. + +.TP +.B \-\-frames\-per\-period=# + +This option uses given value to decide the number of audio data frame in one +read/write operation. The operation is blocked till the number of available +audio data frame exceeds the given value. As a default, 512 audio data frames +is used. + +.TP +.B \-\-periods\-per\-buffer=# + +This option uses given value to decide the size of intermediate buffer between +this program and libffado. As a default, 2 periods per buffer is used. + +.TP +.B \-\-slave + +This option allows this program to run slave mode. In this mode, libffado +adds unit directory into configuration ROM of 1394 OHCI controller where Linux +system runs. The unit directory can be found by the other node on the same bus. +Linux system running on the node can transfer isochronous packet with audio +data frame to the unit. This program can receive the packet and demultiplex the +audio data frame. + +.TP +.B \-\-snoop + +This option allows this program to run snoop mode. In this mode, libffado +listens isochronous channels to which device transfers isochronous packet. When +isochronous communication starts by any unit on the same bus, the packets can +be handled by this program. + +.TP +.B \-\-sched\-priority=# + +This option executes +.I pthread_setschedparam() +in a call of +.I ffado_streaming_init() +to configure +scheduling policy and given value as its priority for threads related to +isochronous communication. +The given value should be within +.I RLIMIT_RTPRIO +parameter of process. Please read +.I getrlimit(2) +for details. + +.SH POSIX SIGNALS +During transmission, +.I SIGINT +and +.I SIGTERM +will close handled files and PCM substream to be going to finish run time. + +.I SIGTSTP +will suspend PCM substream and +.I SIGCONT +will resume it. No XRUNs are expected. With libffado backend, the suspend/resume +is not supported and runtime is aborted immediately. + +The other signals perform default behaviours. + +.SH EXAMPLES + +.PP +.in +4n +.EX +.B $ axfer transfer playback \-d 1 something +.EE +.in +.PP + +The above will transfer audio data frame in \(aqsomething\(aq file for playback +during 1 second. The sample format is detected automatically as a result to +parse \(aqsomething\(aq as long as it\(aqs compliant to one of Microsoft/IBM +RIFF/Wave, Sparc AU, Creative Tech. voice formats. If nothing detected, +.I \-r +, +.I \-c +and +.I \-f +should be given, +or +.I \-f +should be given with special format. + +.PP +.in +4n +.EX +.B $ axfer transfer playback \-r 22050 \-c 1 \-f S16_LE \-t raw something +.EE +.in +.PP + +The above will transfer audio data frame in \(aqsomething\(aq file including no +information of sample format, as sample format of 22050 Hz, monaural, signed 16 +bit little endian PCM for playback. The transmission continues till catching +.I SIGINT +from keyboard or +.I SIGTERM +by +.I kill(1) +\&. + +.PP +.in +4n +.EX +.B $ axfer transfer capture \-d 10 \-f cd something.wav +.EE +.in +.PP + +The above will transfer audio data frame to \(aqsomething.wav\(aq file as +sample format of 44.1 kHz, 2 channels, signed 16 bit little endian PCM, during +10 seconds. The file format is Microsoft/IBM RIFF/Wave according to suffix of +the given +.I filepath +\&. + +.PP +.in +4n +.EX +.B $ axfer transfer capture \-s 1024 \-r 48000 \-c 2 \-f S32_BE \-I \-t au channels +.EE +.in +.PP + +The above will transfer audio data frame as sample format of 48.0 kHz, 2 +channels, signed 32 bit big endian PCM for 1,024 number of data frames to files +named \(aqchannels\-1.au\(aq and \(aqchannels\-2.au\(aq. + +.SH SCHEDULING MODEL + +In a design of ALSA PCM core, runtime of PCM substream supports two modes; +.I period\-wakeup +and +.I no\-period\-wakeup. +These two modes are for different scheduling models. + +.SS IRQ\-based scheduling model + +As a default, +.I period\-wakeup +mode is used. In this mode, in\-kernel drivers should operate hardware to +generate periodical notification for transmission of audio data frame. The +interval of notification is equivalent to the same amount of audio data frame +as one period of buffer, against actual time. + +In a handler assigned to the notification, a helper function of ALSA PCM core +is called to update a position to head of hardware transmission, then compare +it with a position to head of application operation to judge overrun/underrun +(XRUN) and to wake up blocked processes. + +For this purpose, hardware IRQ of controller for serial audio bus such as +Inter\-IC sound is typically used. In this case, the controller generates the +IRQ according to transmission on the serial audio bus. In the handler assigned +to the IRQ, direct media access (DMA) transmission is requested between +dedicated host memory and device memory. + +If target hardware doesn't support this kind of mechanism, the periodical +notification should be emulated by any timer; e.g. hrtimer, kernel timer. +External PCM plugins generated by PCM plugin SDK in alsa\-lib should also +emulate the above behaviour. + +In this mode, PCM applications are programmed according to typical way of I/O +operations. They execute blocking system calls to read/write audio data frame +in buffer of PCM substream, or blocking system calls to wait until any audio +data frame is available. In +.I axfer +, this is called +.I IRQ\-based +scheduling model and a default behaviour. Users can explicitly configure this +mode by usage of +.I \-\-sched\-model +option with +.I irq +value. + +.SS Timer\-based scheduling model + +The +.I no\-period\-wakeup +mode is an optional mode of runtime of PCM substream. The mode assumes a +specific feature of hardware and assist of in\-kernel driver and PCM +applications. In this mode, in\-kernel drivers don't operate hardware to +generate periodical notification for transmission of audio data frame. +The hardware should automatically continue transmission of audio data frame +without periodical operation of the drivers; e.g. according to auto\-triggered +DMA transmission, a chain of registered descriptors. + +In this mode, nothing wakes up blocked processes, therefore PCM applications +should be programmed without any blocking operation. For this reason, this mode +is enabled when the PCM applications explicitly configure hardware parameter to +runtime of PCM substream, to prevent disorder of existing applications. +Additionally, nothing maintains timing for transmission of audio data frame, +therefore the PCM applications should voluntarily handle any timer to queue +audio data frame in buffer of the PCM substream for lapse of time. Furthermore, +instead of driver, the PCM application should call a helper function of ALSA +PCM core to update a position to head of hardware transmission and to check +XRUN. + +In +.I axfer +, this is called +.I timer\-based +scheduling model and available as long as hardware/driver assists +.I no\-period\-wakeup +runtime. Users should explicitly set this mode by usage of +.I \-\-sched\-model +option with +.I timer +value. + +In the scheduling model, PCM applications need to care of available space on +PCM buffer by lapse of time, typically by yielding CPU and wait for +rescheduling. For the yielding, timeout is calculated for preferable amount of +PCM frames to process. This is convenient to a kind of applications, like sound +servers. when an I/O thread of the server wait for the timeout, the other +threads can process audio data frames for server clients. Furthermore, with +usage of rewinding/forwarding, applications can achieve low latency between +transmission position and handling position even if they uses large size of +PCM buffers. + +.SS Advantages and issues + +Ideally, timer\-based scheduling model has some advantages than IRQ\-based +scheduling model. At first, no interrupt context runs for PCM substream. The +PCM substream is handled in any process context only. No need to care of race +conditions between IRQ and process contexts. This reduces some concerns for +some developers of drivers and applications. Secondary, CPU time is not used +for handlers on the interrupt context. The CPU time can be dedicated for the +other tasks. This is good in a point of Time Sharing System. Thirdly, hardware +is not configured to generate interrupts. This is good in a point of reduction +of overall power consumption possibly. + +In either scheduling model, the hardware should allow drivers to read the +number of audio data frame transferred between the dedicated memory and the +device memory for audio serial bus. However, in timer\-based scheduling model, +fine granularity and accuracy of the value is important. Actually hardware +performs transmission between dedicated memory and device memory for a small +batch of audio data frames or bytes. In a view of PCM applications, the +granularity in current transmission is required to decide correct timeout for +each I/O operation. As of Linux kernel v4.21, ALSA PCM interface between +kernel/userspace has no feature to report it. + +.SH COMPATIBILITY TO APLAY + +The +.B transfer +subcommand of +.B axfer +is designed to keep compatibility to aplay(1). However some options below are +not compatible due to several technical reasons. + +.TP +.I \-I, \-\-separate\-channels +This option is supported just for files to store audio data frames corresponding +to each channel. In aplay(1) implementation, this option has an additional +effect to use PCM buffer aligned to non\-interleaved order if a target device +supports. As of 2018, PCM buffer of non\-interleaved order is hardly used by +sound devices. + +.TP +.I \-A, \-\-avail\-min=# +This option indicates threshold to wake up blocked process in a unit of +audio data frame. Against aplay(1) implementation, this option has no effect +with +.I \-\-mmap +option as well as +.I timer +of +.I \-\-sched\-model +option. + +.TP +.I \-R, \-\-start\-delay=# +This option indicates threshold to start prepared PCM substream in a unit of +audio data frame. Against aplay(1) implementation, this option has no effect +with +.I \-\-mmap +option as well as +.I timer +of +.I \-\-sched\-model +option. + +.TP +.I \-T, \-\-stop\-delay=# +This option indicates threshold to stop running PCM substream in a unit of +audio data frame. Against aplay(1) implementation, this option has no effect +with +.I \-\-mmap +option as well as +.I timer +of +.I \-\-sched\-model +option. + +.TP +.I \-\-max\-file\-time=# +This option is unsupported. In aplay(1) implementation, the option has an +effect for capture transmission to save files up to the same number of data +frames as the given value by second unit, or the maximum number of data frames +supported by used file format. When reaching to the limitation, used file is +closed, then new file is opened and audio data frames are written. However, this +option requires extra handling of files and shall increase complexity of main +loop of axfer. + +.TP +.I \-\-use\-strftime=FORMAT +This option is unsupported. In aplay(1) implementation, the option has an effect +for capture transmission to generate file paths according to given format in +which some extra formats are available as well as formats supported by +strftime(3). However, this option requires extra string processing for file +paths and it\(aqs bothersome if written in C language. + +.TP +.I \-\-process\-id\-file=FILEPATH +This option is unsupported. In aplay(1) implementation, the option has an effect +to create a file for given value and write out process ID to it. This file +allows users to get process ID and send any POSIX signal to aplay process. +However, this idea has some troubles for file locking when multiple aplay +processes run with the same file. + +.TP +.I \-V, \-\-vumeter=TYPE +This option is not supported at present. In aplay(1) implementation, this option +has an effect to occupy stdout with some terminal control characters and display +vumeter for monaural and stereo channels. However, some problems lay; this +feature is just for audio data frames with PCM format, this feature brings +disorder of terminal after aborting, stdout is not available for pipeline. + +.TP +.I \-i, \-\-interactive +This option is not supported at present. In aplay(1) implementation, this option +has an effect to occupy stdin for key input and suspend/resume PCM substream +according to pushed enter key. However, this feature requires an additional +input handling in main loop and leave bothersome operation to maintain PCM +substream. + +.TP +.I \-m, \-\-chmap=CH1,CH2,... +ALSA PCM core and control core doesn't support this feature, therefore +remapping should be done in userspace. This brings overhead to align audio +data frames, especially for mmap operation. Furthermore, as of alsa-lib v1.1.8, +some plugins don't support this feature expectedly, thus this option is a lack +of transparent operation. At present, this option is not supported yet not to +confuse users. + +.TP +.I SIGTSTP, SIGCONT +This performs suspend/resume of PCM substream. In aplay(1) implementation, +these operations bring XRUN state to the substream, and suspend/resume is done +in interactive mode in the above. Some developers use the signal for recovery +test from XRUN. At present, no alternative is supported for the test. + +.TP +.I SIGUSR1 +This is not supported. In aplay(1) implementation, this signal is assigned to a +handler to close a current file to store audio data frame and open a new file +to continue processing. However, as well as +.I \-\-max\-file\-time +option, this option should increase complexity of main loop of axfer. + +.SH DESIGN + +.SS Modular structure + +This program consists of three modules; +.I xfer +, +.I mapper +and +.I container +\&. +Each module has an abstraction layer to enable actual implementation. + +.nf + -------- ---------- ------------- +device <-> | xfer | <-> | mapper | <-> | container | <-> file + -------- ---------- ------------- + libasound single wav + libffado multiple au + voc + raw +.fi + +The +.I xfer +module performs actual transmission to devices and nodes. The module can have +several transmission backends. As a default backend, +.I libasound +backend is used to perform transmission via alsa\-lib APIs. The module allows +each backend to parse own command line options. + +The +.I container +module performs to read/write audio data frame via descriptor for file/stream +of multimedia container or raw data. The module automatically detect type of +multimedia container and parse parameters in its metadata of data header. At +present, three types of multimedia containers are supported; Microsoft/IBM +RIFF/Wave ( +.I wav +), Sparc AU ( +.I au +) and Creative Technology voice ( +.I voc +). Additionally, a special container is prepared for raw audio data ( +.I raw +). + +The +.I mapper +module handles buffer layout and alignment for transmission of audio data frame. +The module has two implementations; +.I single +and +.I multiple +\&. +The +.I single +backend uses one container to construct the buffer. The +.I multiple +backend uses several containers to construct it. + +.SS Care of copying audio data frame + +Between the +.I xfer +module and +.I mapper +module, a pointer to buffer including audio data frames is passed. This buffer +has two shapes for interleaved and non\-interleaved order. For the former, the +pointer points to one buffer. For the latter, the pointer points to an array in +which each element points to one buffer. Between the +.I mapper +module and +.I container +module, a pointer to one buffer is passed because supported media containers +including raw type store audio data frames in interleaved order. + +In passing audio data frame between the modules, axfer is programmed to avoid +copying between a buffer to another buffer as much as possible. For example, in +some scenarios below, no copying occurs between modules. + + - xfer(mmap/interleaved), mapper(single), container(any) + - xfer(mmap/non\-interleaved), mapper(multiple), containers(any) + +.SS Unit test + +For each of the +.I mapper +and +.I container +module, unit test is available. To run the tests, execute below command: + +.nf +$ make test +.fi + +Each test iterates writing to file and reading to the file for many times and it +takes long time to finish. Please take care of the execution time if running on +any CI environment. + +.SH SEE ALSO +\fB +axfer(1), +axfer\-list(1), +alsamixer(1), +amixer(1) +\fP + +.SH AUTHOR +Takashi Sakamoto diff --git a/axfer/axfer.1 b/axfer/axfer.1 new file mode 100644 index 0000000..3ae9c8e --- /dev/null +++ b/axfer/axfer.1 @@ -0,0 +1,139 @@ +.TH AXFER 1 "28 November 2018" "alsa\-utils" + +.SH NAME +axfer \- command\-line sound recorder and player for sound devices and nodes +supported by Linux sound subsystem (Advanced Linux Sound Architecture, also +known as ALSA). + +.SH SYNOPSYS + +.B axfer +.I subcommand direction options + +subcommand = +.B transfer +| +.B list +| +.B version +| +.B help + +direction = +.B capture +| +.B playback + +options = ( depends on +.I subcommand +) + +.SH DESCRIPTION +The +.B axfer +is a command\-line recorder and player to transfer audio data frame between +sound devices/nodes and files/stdin/stdout. + +.SH OPTIONS + +.SS Subcommand + +.TP +.B transfer +Performs transmission of audio data frame. Its detail is described in +.B axfer\-transfer(1) +manual. + +.TP +.B list +Dumps lists of available sound devices and nodes. Its detail is described in +.B axfer\-list(1) +manual. + +.TP +.B version +Prints version of this application (as the same version as alsa\-utils package). + +.TP +.B help +Prints a short message about subcommands for users to enter this application. + +.SS Direction + +.TP +.B capture +Operates for capture transmission. + +.TP +.B playback +Operates for playback transmission. + +.SH EXIT STATUS + +.I EXIT_SUCCESS +(0) if run time successfully finished, else +.I EXIT_FAILURE +(1). + +.SH UNIT TEST + +This program has unit tests for internal implementation. Please refer to the +manual of +.I axfer-transfer +for details. + +.SH COMPATIBILITY TO APLAY + +The +.I axfer +is designed to be compatible to aplay(1) as much as possible. In command line, +executions of aplay/arecord files under $PATH runs axfer with compatibility +mode if filesystem has symbolic link from the aplay/arecord to axfer. + +.PP +.in +4n +.EX +$ ln \-s aplay axfer +$ ln \-s arecord axfer +.EE +.in +.PP + +.SS A string to which arg[0] points +When args[0] in run time points to string ended with \(aqaplay\(aq, it has the +same meaning of +.I playback +direction. When it points to string ended with \(aqarecord\(aq, it has the same +meaning of +.I capture +direction. + +.SS Options acknowledged as list subcommand +Options of +.I \-l +, +.I \-\-list\-devices +, +.I \-L +, +.I \-\-list\-pcms +are acknowledged as +.I list +subcommand. Without them, the run time performs +.I transfer +subcommand. + +.SH REPORTING BUGS +Report any bugs to mailing list of ALSA community + where the development and maintenance is +primarily done. Bug tracking service of alsa\-utils repository on github.com is +also available. + +.SH SEE ALSO +.B axfer\-transfer(1), +.B axfer\-list(1), +.B alsamixer(1), +.B amixer(1) + +.SH AUTHOR +Takashi Sakamoto diff --git a/axfer/container-au.c b/axfer/container-au.c new file mode 100644 index 0000000..cccbebd --- /dev/null +++ b/axfer/container-au.c @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-au.c - a parser/builder for a container of Sun Audio File. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +// Not portable to all of UNIX platforms. +#include + +// Reference: +// * http://pubs.opengroup.org/external/auformat.html + +#define AU_MAGIC ".snd" +#define UNKNOWN_SIZE UINT32_MAX + +enum code_id { + CODE_ID_CCIT_MU_LAW_BE = 0x01, + CODE_ID_GENERIC_MBLA_S8 = 0x02, + CODE_ID_GENERIC_MBLA_S16_BE = 0x03, + CODE_ID_GENERIC_MBLA_S32_BE = 0x05, + CODE_ID_IEEE754_FLOAT_S32_BE = 0x06, + CODE_ID_IEEE754_DOUBLE_S64_BE = 0x07, + CODE_ID_CCIT_ADPCM_G721_4BIT_BE = 0x17, + CODE_ID_CCIT_ADPCM_G723_3BIT_BE = 0x19, + CODE_ID_CCIT_A_LAW_BE = 0x1b, +}; + +struct format_map { + enum code_id code_id; + snd_pcm_format_t format; +}; + +static const struct format_map format_maps[] = { + {CODE_ID_GENERIC_MBLA_S8, SND_PCM_FORMAT_S8}, + {CODE_ID_GENERIC_MBLA_S16_BE, SND_PCM_FORMAT_S16_BE}, + {CODE_ID_GENERIC_MBLA_S32_BE, SND_PCM_FORMAT_S32_BE}, + {CODE_ID_IEEE754_FLOAT_S32_BE, SND_PCM_FORMAT_FLOAT_BE}, + {CODE_ID_IEEE754_DOUBLE_S64_BE, SND_PCM_FORMAT_FLOAT64_BE}, + // CODE_ID_CCIT_ADPCM_G721_4BIT_BE is not supported by ALSA. + // CODE_ID_CCIT_ADPCM_G723_3BIT_BE is not supported due to width of + // its sample. + {CODE_ID_CCIT_A_LAW_BE, SND_PCM_FORMAT_A_LAW}, + {CODE_ID_CCIT_MU_LAW_BE, SND_PCM_FORMAT_MU_LAW}, +}; + +struct container_header { + uint8_t magic[4]; + uint32_t hdr_size; + uint32_t data_size; + uint32_t code_id; + uint32_t frames_per_second; + uint32_t samples_per_frame; +}; + +struct container_annotation { + uint32_t chunks[0]; +}; + +struct parser_state { + enum code_id code_id; + unsigned int samples_per_frame; + unsigned int bytes_per_sample; +}; + +static int au_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + struct container_header header; + enum code_id code_id; + unsigned int i; + int err; + + // Parse header. 4 bytes are enough to detect supported containers. + memcpy(&header.magic, cntr->magic, sizeof(cntr->magic)); + err = container_recursive_read(cntr, + (char *)&header + sizeof(cntr->magic), + sizeof(header) - sizeof(cntr->magic)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + if (memcmp(header.magic, AU_MAGIC, sizeof(header.magic)) != 0) + return -EINVAL; + if (be32toh(header.hdr_size) != sizeof(struct container_header)) + return -EINVAL; + + code_id = be32toh(header.code_id); + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].code_id == code_id) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + *format = format_maps[i].format; + *frames_per_second = be32toh(header.frames_per_second); + *samples_per_frame = be32toh(header.samples_per_frame); + + state->code_id = code_id; + state->samples_per_frame = *samples_per_frame; + state->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + + *byte_count = be32toh(header.data_size); + + return 0; +} + +struct builder_state { + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_second; + enum code_id code_id; +}; + +static void build_container_header(struct builder_state *state, + struct container_header *header, + unsigned int frames_per_second, + uint64_t byte_count) +{ + memcpy(header->magic, AU_MAGIC, sizeof(header->magic)); + header->hdr_size = htobe32(sizeof(struct container_header)); + header->data_size = htobe32(byte_count); + header->code_id = htobe32(state->code_id); + header->frames_per_second = htobe32(frames_per_second); + header->samples_per_frame = htobe32(state->samples_per_frame); +} + +static int write_container_header(struct container_context *cntr, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + struct container_header header; + + build_container_header(state, &header, state->frames_per_second, + byte_count); + + return container_recursive_write(cntr, &header, sizeof(header)); +} + +static int au_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct builder_state *status = cntr->private_data; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].format == *format) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + status->code_id = format_maps[i].code_id; + status->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + status->frames_per_second = *frames_per_second; + status->samples_per_frame = *samples_per_frame; + + return write_container_header(cntr, *byte_count); +} + +static int au_builder_post_process(struct container_context *cntr, + uint64_t handled_byte_count) +{ + int err; + + err = container_seek_offset(cntr, 0); + if (err < 0) + return err; + + return write_container_header(cntr, handled_byte_count); +} + +const struct container_parser container_parser_au = { + .format = CONTAINER_FORMAT_AU, + .magic = AU_MAGIC, + .max_size = UINT32_MAX, + .ops = { + .pre_process = au_parser_pre_process, + }, + .private_size = sizeof(struct parser_state), +}; + +const struct container_builder container_builder_au = { + .format = CONTAINER_FORMAT_AU, + .max_size = UINT32_MAX, + .ops = { + .pre_process = au_builder_pre_process, + .post_process = au_builder_post_process, + }, + .private_size = sizeof(struct builder_state), +}; diff --git a/axfer/container-raw.c b/axfer/container-raw.c new file mode 100644 index 0000000..1886045 --- /dev/null +++ b/axfer/container-raw.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-raw.c - a parser/builder for a container with raw data frame. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +#include +#include +#include + +static int raw_builder_pre_process(struct container_context *cntr ATTRIBUTE_UNUSED, + snd_pcm_format_t *format ATTRIBUTE_UNUSED, + unsigned int *samples_per_frame ATTRIBUTE_UNUSED, + unsigned int *frames_per_second ATTRIBUTE_UNUSED, + uint64_t *byte_count) +{ + *byte_count = UINT64_MAX; + + return 0; +} + +static int raw_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format ATTRIBUTE_UNUSED, + unsigned int *samples_per_frame ATTRIBUTE_UNUSED, + unsigned int *frames_per_second ATTRIBUTE_UNUSED, + uint64_t *byte_count) +{ + struct stat buf = {0}; + int err; + + if (cntr->stdio) { + *byte_count = UINT64_MAX; + return 0; + } + + err = fstat(cntr->fd, &buf); + if (err < 0) + return err; + + *byte_count = buf.st_size; + if (*byte_count == 0) + *byte_count = UINT64_MAX; + + return 0; +} + +const struct container_parser container_parser_raw = { + .format = CONTAINER_FORMAT_RAW, + .max_size = UINT64_MAX, + .ops = { + .pre_process = raw_parser_pre_process, + }, +}; + +const struct container_builder container_builder_raw = { + .format = CONTAINER_FORMAT_RAW, + .max_size = UINT64_MAX, + .ops = { + .pre_process = raw_builder_pre_process, + }, +}; diff --git a/axfer/container-riff-wave.c b/axfer/container-riff-wave.c new file mode 100644 index 0000000..f62cdbd --- /dev/null +++ b/axfer/container-riff-wave.c @@ -0,0 +1,574 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-riff-wave.c - a parser/builder for a container of RIFF/Wave File. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +// Not portable to all of UNIX platforms. +#include + +// References: +// - 'Resource Interchange File Format (RIFF)' at msdn.microsoft.com +// - 'Multiple channel audio data and WAVE files' at msdn.microsoft.com +// - RFC 2361 'WAVE and AVI Codec Registries' at ietf.org +// - 'mmreg.h' in Wine project +// - 'mmreg.h' in ReactOS project + +#define RIFF_MAGIC "RIF" // A common part. + +#define RIFF_CHUNK_ID_LE "RIFF" +#define RIFF_CHUNK_ID_BE "RIFX" +#define RIFF_FORM_WAVE "WAVE" +#define FMT_SUBCHUNK_ID "fmt " +#define DATA_SUBCHUNK_ID "data" + +// See 'WAVE and AVI Codec Registries (Historic Registry)' in 'iana.org'. +// https://www.iana.org/assignments/wave-avi-codec-registry/ +enum wave_format { + WAVE_FORMAT_PCM = 0x0001, + WAVE_FORMAT_ADPCM = 0x0002, + WAVE_FORMAT_IEEE_FLOAT = 0x0003, + WAVE_FORMAT_ALAW = 0x0006, + WAVE_FORMAT_MULAW = 0x0007, + WAVE_FORMAT_G723_ADPCM = 0x0014, + // The others are not supported. +}; + +struct format_map { + enum wave_format wformat; + snd_pcm_format_t format; +}; + +static const struct format_map format_maps[] = { + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_U8}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S16_LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S16_BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S32_LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S32_BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_3LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_3BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S20_3LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S20_3BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S18_3LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S18_3BE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT_LE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT_BE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT64_LE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT64_BE}, + {WAVE_FORMAT_ALAW, SND_PCM_FORMAT_A_LAW}, + {WAVE_FORMAT_MULAW, SND_PCM_FORMAT_MU_LAW}, + // Below sample formats are not currently supported, due to width of + // its sample. + // - WAVE_FORMAT_ADPCM + // - WAVE_FORMAT_G723_ADPCM + // - WAVE_FORMAT_G723_ADPCM + // - WAVE_FORMAT_G723_ADPCM + // - WAVE_FORMAT_G723_ADPCM +}; + +struct riff_chunk { + uint8_t id[4]; + uint32_t size; + + uint8_t data[0]; +}; + +struct riff_chunk_data { + uint8_t id[4]; + + uint8_t subchunks[0]; +}; + +struct riff_subchunk { + uint8_t id[4]; + uint32_t size; + + uint8_t data[0]; +}; + +struct wave_fmt_subchunk { + uint8_t id[4]; + uint32_t size; + + uint16_t format; + uint16_t samples_per_frame; + uint32_t frames_per_second; + uint32_t average_bytes_per_second; + uint16_t bytes_per_frame; + uint16_t bits_per_sample; + uint8_t extension[0]; +}; + +struct wave_data_subchunk { + uint8_t id[4]; + uint32_t size; + + uint8_t frames[0]; +}; + +struct parser_state { + bool be; + enum wave_format format; + unsigned int samples_per_frame; + unsigned int frames_per_second; + unsigned int average_bytes_per_second; + unsigned int bytes_per_frame; + unsigned int bytes_per_sample; + unsigned int avail_bits_in_sample; + unsigned int byte_count; +}; + +static int parse_riff_chunk_header(struct parser_state *state, + struct riff_chunk *chunk, + uint64_t *byte_count) +{ + if (!memcmp(chunk->id, RIFF_CHUNK_ID_BE, sizeof(chunk->id))) + state->be = true; + else if (!memcmp(chunk->id, RIFF_CHUNK_ID_LE, sizeof(chunk->id))) + state->be = false; + else + return -EINVAL; + + if (state->be) + *byte_count = be32toh(chunk->size); + else + *byte_count = le32toh(chunk->size); + + return 0; +} + +static int parse_riff_chunk(struct container_context *cntr, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + union { + struct riff_chunk chunk; + struct riff_chunk_data chunk_data; + } buf = {0}; + int err; + + // Chunk header. 4 bytes were alread read to detect container type. + memcpy(buf.chunk.id, cntr->magic, sizeof(cntr->magic)); + err = container_recursive_read(cntr, + (char *)&buf.chunk + sizeof(cntr->magic), + sizeof(buf.chunk) - sizeof(cntr->magic)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + err = parse_riff_chunk_header(state, &buf.chunk, byte_count); + if (err < 0) + return err; + + // Chunk data header. + err = container_recursive_read(cntr, &buf, sizeof(buf.chunk_data)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + if (memcmp(buf.chunk_data.id, RIFF_FORM_WAVE, + sizeof(buf.chunk_data.id))) + return -EINVAL; + + return 0; +} + +static int parse_wave_fmt_subchunk(struct parser_state *state, + struct wave_fmt_subchunk *subchunk) +{ + if (state->be) { + state->format = be16toh(subchunk->format); + state->samples_per_frame = be16toh(subchunk->samples_per_frame); + state->frames_per_second = be32toh(subchunk->frames_per_second); + state->average_bytes_per_second = + be32toh(subchunk->average_bytes_per_second); + state->bytes_per_frame = be16toh(subchunk->bytes_per_frame); + state->avail_bits_in_sample = + be16toh(subchunk->bits_per_sample); + } else { + state->format = le16toh(subchunk->format); + state->samples_per_frame = le16toh(subchunk->samples_per_frame); + state->frames_per_second = le32toh(subchunk->frames_per_second); + state->average_bytes_per_second = + le32toh(subchunk->average_bytes_per_second); + state->bytes_per_frame = le16toh(subchunk->bytes_per_frame); + state->avail_bits_in_sample = + le16toh(subchunk->bits_per_sample); + } + + if (state->average_bytes_per_second != + state->bytes_per_frame * state->frames_per_second) + return -EINVAL; + + return 0; +} + +static int parse_wave_data_subchunk(struct parser_state *state, + struct wave_data_subchunk *subchunk) +{ + if (state->be) + state->byte_count = be32toh(subchunk->size); + else + state->byte_count = le32toh(subchunk->size); + + return 0; +} + +static int parse_wave_subchunk(struct container_context *cntr) +{ + union { + struct riff_subchunk subchunk; + struct wave_fmt_subchunk fmt_subchunk; + struct wave_data_subchunk data_subchunk; + } buf = {0}; + enum { + SUBCHUNK_TYPE_UNKNOWN = -1, + SUBCHUNK_TYPE_FMT, + SUBCHUNK_TYPE_DATA, + } subchunk_type; + struct parser_state *state = cntr->private_data; + unsigned int required_size; + unsigned int subchunk_data_size; + int err; + + while (1) { + err = container_recursive_read(cntr, &buf, + sizeof(buf.subchunk)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + // Calculate the size of subchunk data. + if (state->be) + subchunk_data_size = be32toh(buf.subchunk.size); + else + subchunk_data_size = le32toh(buf.subchunk.size); + + // Detect type of subchunk. + if (!memcmp(buf.subchunk.id, FMT_SUBCHUNK_ID, + sizeof(buf.subchunk.id))) { + subchunk_type = SUBCHUNK_TYPE_FMT; + } else if (!memcmp(buf.subchunk.id, DATA_SUBCHUNK_ID, + sizeof(buf.subchunk.id))) { + subchunk_type = SUBCHUNK_TYPE_DATA; + } else { + subchunk_type = SUBCHUNK_TYPE_UNKNOWN; + } + + if (subchunk_type != SUBCHUNK_TYPE_UNKNOWN) { + // Parse data of this subchunk. + if (subchunk_type == SUBCHUNK_TYPE_FMT) { + required_size = + sizeof(struct wave_fmt_subchunk) - + sizeof(struct riff_chunk); + } else { + required_size = + sizeof(struct wave_data_subchunk)- + sizeof(struct riff_chunk); + } + + if (subchunk_data_size < required_size) + return -EINVAL; + + err = container_recursive_read(cntr, &buf.subchunk.data, + required_size); + if (err < 0) + return err; + if (cntr->eof) + return 0; + subchunk_data_size -= required_size; + + if (subchunk_type == SUBCHUNK_TYPE_FMT) { + err = parse_wave_fmt_subchunk(state, + &buf.fmt_subchunk); + } else if (subchunk_type == SUBCHUNK_TYPE_DATA) { + err = parse_wave_data_subchunk(state, + &buf.data_subchunk); + } + if (err < 0) + return err; + + // Found frame data. + if (subchunk_type == SUBCHUNK_TYPE_DATA) + break; + } + + // Go to next subchunk. + while (subchunk_data_size > 0) { + unsigned int consume; + + if (subchunk_data_size > sizeof(buf)) + consume = sizeof(buf); + else + consume = subchunk_data_size; + + err = container_recursive_read(cntr, &buf, consume); + if (err < 0) + return err; + if (cntr->eof) + return 0; + subchunk_data_size -= consume; + } + } + + return 0; +} + +static int parse_riff_wave_format(struct container_context *cntr) +{ + uint64_t byte_count; + int err; + + err = parse_riff_chunk(cntr, &byte_count); + if (err < 0) + return err; + + err = parse_wave_subchunk(cntr); + if (err < 0) + return err; + + return 0; +} + +static int wave_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + int phys_width; + const struct format_map *map; + unsigned int i; + int err; + + err = parse_riff_wave_format(cntr); + if (err < 0) + return err; + + phys_width = 8 * state->average_bytes_per_second / + state->samples_per_frame / state->frames_per_second; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + map = &format_maps[i]; + if (state->format != map->wformat) + continue; + if ((int)state->avail_bits_in_sample != + snd_pcm_format_width(map->format)) + continue; + if (phys_width != snd_pcm_format_physical_width(map->format)) + continue; + + if (state->be && snd_pcm_format_big_endian(map->format) != 1) + continue; + + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + // Set parameters. + *format = format_maps[i].format; + *samples_per_frame = state->samples_per_frame; + *frames_per_second = state->frames_per_second; + *byte_count = state->byte_count; + + return 0; +} + +struct builder_state { + bool be; + enum wave_format format; + unsigned int avail_bits_in_sample; + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_second; +}; + +static void build_riff_chunk_header(struct riff_chunk *chunk, + uint64_t byte_count, bool be) +{ + uint64_t data_size = sizeof(struct riff_chunk_data) + + sizeof(struct wave_fmt_subchunk) + + sizeof(struct wave_data_subchunk) + byte_count; + + if (be) { + memcpy(chunk->id, RIFF_CHUNK_ID_BE, sizeof(chunk->id)); + chunk->size = htobe32(data_size); + } else { + memcpy(chunk->id, RIFF_CHUNK_ID_LE, sizeof(chunk->id)); + chunk->size = htole32(data_size); + } +} + +static void build_subchunk_header(struct riff_subchunk *subchunk, + const char *const form, uint64_t size, + bool be) +{ + memcpy(subchunk->id, form, sizeof(subchunk->id)); + if (be) + subchunk->size = htobe32(size); + else + subchunk->size = htole32(size); +} + +static void build_wave_format_subchunk(struct wave_fmt_subchunk *subchunk, + struct builder_state *state) +{ + unsigned int bytes_per_frame = + state->bytes_per_sample * state->samples_per_frame; + unsigned int average_bytes_per_second = state->bytes_per_sample * + state->samples_per_frame * state->frames_per_second; + uint64_t size; + + // No extensions. + size = sizeof(struct wave_fmt_subchunk) - sizeof(struct riff_subchunk); + build_subchunk_header((struct riff_subchunk *)subchunk, FMT_SUBCHUNK_ID, + size, state->be); + + if (state->be) { + subchunk->format = htobe16(state->format); + subchunk->samples_per_frame = htobe16(state->samples_per_frame); + subchunk->frames_per_second = htobe32(state->frames_per_second); + subchunk->average_bytes_per_second = + htobe32(average_bytes_per_second); + subchunk->bytes_per_frame = htobe16(bytes_per_frame); + subchunk->bits_per_sample = + htobe16(state->avail_bits_in_sample); + } else { + subchunk->format = htole16(state->format); + subchunk->samples_per_frame = htole16(state->samples_per_frame); + subchunk->frames_per_second = htole32(state->frames_per_second); + subchunk->average_bytes_per_second = + htole32(average_bytes_per_second); + subchunk->bytes_per_frame = htole16(bytes_per_frame); + subchunk->bits_per_sample = + htole16(state->avail_bits_in_sample); + } +} + +static void build_wave_data_subchunk(struct wave_data_subchunk *subchunk, + uint64_t byte_count, bool be) +{ + build_subchunk_header((struct riff_subchunk *)subchunk, + DATA_SUBCHUNK_ID, byte_count, be); +} + +static int write_riff_chunk_for_wave(struct container_context *cntr, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + union { + struct riff_chunk chunk; + struct riff_chunk_data chunk_data; + struct wave_fmt_subchunk fmt_subchunk; + struct wave_data_subchunk data_subchunk; + } buf = {0}; + uint64_t total_byte_count; + int err; + + // Chunk header. + total_byte_count = sizeof(struct riff_chunk_data) + + sizeof(struct wave_fmt_subchunk) + + sizeof(struct wave_data_subchunk); + if (byte_count > cntr->max_size - total_byte_count) + total_byte_count = cntr->max_size; + else + total_byte_count += byte_count; + build_riff_chunk_header(&buf.chunk, total_byte_count, state->be); + err = container_recursive_write(cntr, &buf, sizeof(buf.chunk)); + if (err < 0) + return err; + + // Chunk data header. + memcpy(buf.chunk_data.id, RIFF_FORM_WAVE, sizeof(buf.chunk_data.id)); + err = container_recursive_write(cntr, &buf, sizeof(buf.chunk_data)); + if (err < 0) + return err; + + // A subchunk in the chunk data for WAVE format. + build_wave_format_subchunk(&buf.fmt_subchunk, state); + err = container_recursive_write(cntr, &buf, sizeof(buf.fmt_subchunk)); + if (err < 0) + return err; + + // A subchunk in the chunk data for WAVE data. + build_wave_data_subchunk(&buf.data_subchunk, byte_count, state->be); + return container_recursive_write(cntr, &buf, sizeof(buf.data_subchunk)); +} + +static int wave_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct builder_state *state = cntr->private_data; + unsigned int i; + + // Validate parameters. + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].format == *format) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + state->format = format_maps[i].wformat; + state->avail_bits_in_sample = snd_pcm_format_width(*format); + state->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + state->samples_per_frame = *samples_per_frame; + state->frames_per_second = *frames_per_second; + + state->be = (snd_pcm_format_big_endian(*format) == 1); + + return write_riff_chunk_for_wave(cntr, *byte_count); +} + +static int wave_builder_post_process(struct container_context *cntr, + uint64_t handled_byte_count) +{ + int err; + + err = container_seek_offset(cntr, 0); + if (err < 0) + return err; + + return write_riff_chunk_for_wave(cntr, handled_byte_count); +} + +const struct container_parser container_parser_riff_wave = { + .format = CONTAINER_FORMAT_RIFF_WAVE, + .magic = RIFF_MAGIC, + .max_size = UINT32_MAX - + sizeof(struct riff_chunk_data) - + sizeof(struct wave_fmt_subchunk) - + sizeof(struct wave_data_subchunk), + .ops = { + .pre_process = wave_parser_pre_process, + }, + .private_size = sizeof(struct parser_state), +}; + +const struct container_builder container_builder_riff_wave = { + .format = CONTAINER_FORMAT_RIFF_WAVE, + .max_size = UINT32_MAX - + sizeof(struct riff_chunk_data) - + sizeof(struct wave_fmt_subchunk) - + sizeof(struct wave_data_subchunk), + .ops = { + .pre_process = wave_builder_pre_process, + .post_process = wave_builder_post_process, + }, + .private_size = sizeof(struct builder_state), +}; diff --git a/axfer/container-voc.c b/axfer/container-voc.c new file mode 100644 index 0000000..f169a1a --- /dev/null +++ b/axfer/container-voc.c @@ -0,0 +1,835 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-voc.c - a parser/builder for a container of Creative Voice File. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +// Not portable to all of UNIX platforms. +#include + +// References: +// - http://sox.sourceforge.net/ + +#define VOC_MAGIC "Creative Voice File\x1a" +#define VOC_VERSION_1_10 0x010a +#define VOC_VERSION_1_20 0x0114 + +enum block_type { + BLOCK_TYPE_TERMINATOR = 0x00, + BLOCK_TYPE_V110_DATA = 0x01, + BLOCK_TYPE_CONTINUOUS_DATA = 0x02, + BLOCK_TYPE_SILENCE = 0x03, + BLOCK_TYPE_MARKER = 0x04, + BLOCK_TYPE_STRING = 0x05, + BLOCK_TYPE_REPEAT_START = 0x06, + BLOCK_TYPE_REPEAT_END = 0x07, + BLOCK_TYPE_EXTENDED_V110_FORMAT = 0x08, + BLOCK_TYPE_V120_DATA = 0x09, +}; + +enum code_id { + // Version 1.10. + CODE_ID_GENERIC_MBLA_U8 = 0x00, + CODE_ID_CREATIVE_ADPCM_8BIT_TO_4BIT_LE = 0x01, + CODE_ID_CREATIVE_ADPCM_8BIT_TO_3BIT_LE = 0x02, + CODE_ID_CREATIVE_ADPCM_8BIT_TO_2BIT_LE = 0x03, + // Version 1.20. + CODE_ID_GENERIC_MBLA_S16_LE = 0x04, + CODE_ID_CCIT_A_LAW_LE = 0x06, + CODE_ID_CCIT_MU_LAW_LE = 0x07, + CODE_ID_CREATIVE_ADPCM_16BIT_TO_4BIT_LE = 0x2000, +}; + +struct format_map { + unsigned int minimal_version; + enum code_id code_id; + snd_pcm_format_t format; +}; + +static const struct format_map format_maps[] = { + {VOC_VERSION_1_10, CODE_ID_GENERIC_MBLA_U8, SND_PCM_FORMAT_U8}, + {VOC_VERSION_1_20, CODE_ID_GENERIC_MBLA_S16_LE, SND_PCM_FORMAT_S16_LE}, + {VOC_VERSION_1_20, CODE_ID_CCIT_A_LAW_LE, SND_PCM_FORMAT_A_LAW}, + {VOC_VERSION_1_20, CODE_ID_CCIT_MU_LAW_LE, SND_PCM_FORMAT_MU_LAW}, + // The other formats are not supported by ALSA. +}; + +struct container_header { + uint8_t magic[20]; + uint16_t hdr_size; + uint16_t version; + uint16_t version_compr; +}; + +// A format for data blocks except for terminator type. +struct block_header { + uint8_t type; + uint8_t size[3]; + + uint8_t data[0]; +}; + +// Data block for terminator type has an exceptional format. +struct block_terminator { + uint8_t type; +}; + +struct time_const { + unsigned int frames_per_second; + uint16_t code; +}; + +static const struct time_const v110_time_consts[] = { + {5512, 74}, + {8000, 130}, + {11025, 165}, + {16000, 193}, + {22050, 210}, + {32000, 224}, + {44100, 233}, + {48000, 235}, + {64000, 240}, + // Time constant for the upper sampling rate is not identical. +}; + +static const struct time_const ex_v110_time_consts[] = { + {5512, 19092}, + {8000, 33536}, + {11025, 42317}, + {16000, 49536}, + {22050, 53927}, + {32000, 57536}, + {44100, 59732}, + {48000, 60203}, + {64000, 61536}, + {88200, 62634}, + {96000, 62870}, + {176400, 64085}, + {192000, 64203}, + // This support up to 192.0 kHz. The rest is for cases with 2ch. + {352800, 64811}, + {384000, 64870}, +}; + +// v1.10 format: +// - monaural. +// - frames_per_second = 1,000,000 / (256 - time_const) +struct block_v110_data { + uint8_t type; + uint8_t size[3]; // Equals to (2 + the size of frames). + + uint8_t time_const; + uint8_t code_id; + uint8_t frames[0]; // Aligned to little-endian. +}; + +struct block_continuous_data { + uint8_t type; + uint8_t size[3]; // Equals to the size of frames. + + uint8_t frames[0]; // Aligned to little-endian. +}; + +// v1.10 format: +// - monaural. +// - frames_per_second = 1,000,000 / (256 - time_const). +struct block_silence { + uint8_t type; + uint8_t size[3]; // Equals to 3. + + uint16_t frame_count; + uint8_t time_const; +}; + +struct block_marker { + uint8_t type; + uint8_t size[3]; // Equals to 2. + + uint16_t mark; +}; + +struct block_string { + uint8_t type; + uint8_t size[3]; // Equals to the length of string with 0x00. + + uint8_t chars[0]; +}; + +struct block_repeat_start { + uint8_t type; + uint8_t size[3]; // Equals to 2. + + uint16_t count; +}; + +struct block_repeat_end { + uint8_t type; + uint8_t size[3]; // Equals to 0. +}; + +// Extended v1.10 format: +// - manaural/stereo. +// - frames_per_second = +// 256,000,000 / (samples_per_frame * (65536 - time_const)). +// - Appear just before v110_data block. +struct block_extended_v110_format { + uint8_t type; + uint8_t size[3]; // Equals to 4. + + uint16_t time_const; + uint8_t code_id; + uint8_t ch_mode; // 0 is monaural, 1 is stereo. +}; + +// v1.20 format: +// - monaural/stereo. +// - 8/16 bits_per_sample. +// - time_const is not used. +// - code_id is extended. +struct block_v120_format { + uint8_t type; + uint8_t size[3]; // Equals to (12 + ). + + uint32_t frames_per_second; + uint8_t bits_per_sample; + uint8_t samples_per_frame; + uint16_t code_id; + uint8_t reserved[4]; + + uint8_t frames[0]; // Aligned to little-endian. +}; + +// Aligned to little endian order but 24 bits field. +static uint32_t parse_block_data_size(uint8_t fields[3]) +{ + return (fields[2] << 16) | (fields[1] << 8) | fields[0]; +} + +static void build_block_data_size(uint8_t fields[3], unsigned int size) +{ + fields[0] = (size & 0x0000ff); + fields[1] = (size & 0x00ff00) >> 8; + fields[2] = (size & 0xff0000) >> 16; +} + +static int build_time_constant(unsigned int frames_per_second, + unsigned int samples_per_frame, uint16_t *code, + bool extended) +{ + unsigned int i; + + // 16 bits are available for this purpose. + if (extended) { + if (samples_per_frame > 2) + return -EINVAL; + frames_per_second *= samples_per_frame; + + for (i = 0; i < ARRAY_SIZE(ex_v110_time_consts); ++i) { + if (ex_v110_time_consts[i].frames_per_second == + frames_per_second) + break; + } + if (i < ARRAY_SIZE(ex_v110_time_consts) && + frames_per_second <= 192000) { + *code = ex_v110_time_consts[i].code; + } else { + *code = 65536 - 256000000 / frames_per_second; + } + } else { + if (samples_per_frame != 1) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(v110_time_consts); ++i) { + if (v110_time_consts[i].frames_per_second == + frames_per_second) + break; + } + // Should be within 8 bit. + if (i < ARRAY_SIZE(v110_time_consts)) + *code = (uint8_t)v110_time_consts[i].code; + else + *code = 256 - 1000000 / frames_per_second; + } + + return 0; +} + +static unsigned int parse_time_constant(uint16_t code, + unsigned int samples_per_frame, + unsigned int *frames_per_second, + bool extended) +{ + unsigned int i; + + if (extended) { + if (samples_per_frame > 2) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(ex_v110_time_consts); ++i) { + if (ex_v110_time_consts[i].code == code || + ex_v110_time_consts[i].code - 1 == code) + break; + } + if (i < ARRAY_SIZE(ex_v110_time_consts)) { + *frames_per_second = + ex_v110_time_consts[i].frames_per_second / + samples_per_frame; + } else { + *frames_per_second = 256000000 / samples_per_frame / + (65536 - code); + } + } else { + if (samples_per_frame != 1) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(v110_time_consts); ++i) { + if (v110_time_consts[i].code == code || + v110_time_consts[i].code - 1 == code) + break; + } + if (i < ARRAY_SIZE(v110_time_consts)) { + *frames_per_second = + v110_time_consts[i].frames_per_second; + } else { + *frames_per_second = 1000000 / (256 - code); + } + } + + return 0; +} + +struct parser_state { + unsigned int version; + bool extended; + + unsigned int frames_per_second; + unsigned int samples_per_frame; + unsigned int bytes_per_sample; + enum code_id code_id; + uint32_t byte_count; +}; + +static int parse_container_header(struct parser_state *state, + struct container_header *header) +{ + uint16_t hdr_size; + uint16_t version; + uint16_t version_compr; + + hdr_size = le16toh(header->hdr_size); + version = le16toh(header->version); + version_compr = le16toh(header->version_compr); + + if (memcmp(header->magic, VOC_MAGIC, sizeof(header->magic))) + return -EIO; + + if (hdr_size != sizeof(*header)) + return -EIO; + + if (version_compr != 0x1234 + ~version) + return -EIO; + + if (version != VOC_VERSION_1_10 && version != VOC_VERSION_1_20) + return -EIO; + + state->version = version; + + return 0; +} + +static bool check_code_id(uint8_t code_id, unsigned int version) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (code_id != format_maps[i].code_id) + continue; + if (version >= format_maps[i].minimal_version) + return true; + } + + return false; +} + +static int parse_v120_format_block(struct parser_state *state, + struct block_v120_format *block) +{ + state->frames_per_second = le32toh(block->frames_per_second); + state->bytes_per_sample = block->bits_per_sample / 8; + state->samples_per_frame = block->samples_per_frame; + state->code_id = le16toh(block->code_id); + state->byte_count = parse_block_data_size(block->size) - 12; + + if (!check_code_id(state->code_id, VOC_VERSION_1_20)) + return -EIO; + + return 0; +} + +static int parse_extended_v110_format(struct parser_state *state, + struct block_extended_v110_format *block) +{ + unsigned int time_const; + unsigned int frames_per_second; + int err; + + state->code_id = block->code_id; + if (!check_code_id(state->code_id, VOC_VERSION_1_10)) + return -EIO; + + if (block->ch_mode == 0) + state->samples_per_frame = 1; + else if (block->ch_mode == 1) + state->samples_per_frame = 2; + else + return -EIO; + + time_const = le16toh(block->time_const); + err = parse_time_constant(time_const, state->samples_per_frame, + &frames_per_second, true); + if (err < 0) + return err; + state->frames_per_second = frames_per_second; + + state->extended = true; + + return 0; +} + +static int parse_v110_data(struct parser_state *state, + struct block_v110_data *block) +{ + unsigned int time_const; + unsigned int frames_per_second; + int err; + + if (!state->extended) { + state->code_id = block->code_id; + if (!check_code_id(state->code_id, VOC_VERSION_1_10)) + return -EIO; + + time_const = block->time_const; + err = parse_time_constant(time_const, 1, &frames_per_second, + false); + if (err < 0) + return err; + state->frames_per_second = frames_per_second; + state->samples_per_frame = 1; + } + + state->bytes_per_sample = 1; + state->byte_count = parse_block_data_size(block->size) - 2; + + return 0; +} + +static int detect_container_version(struct container_context *cntr) +{ + struct parser_state *state = cntr->private_data; + struct container_header header = {0}; + int err; + + // 4 bytes were alread read to detect container type. + memcpy(&header.magic, cntr->magic, sizeof(cntr->magic)); + err = container_recursive_read(cntr, + (char *)&header + sizeof(cntr->magic), + sizeof(header) - sizeof(cntr->magic)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + return parse_container_header(state, &header); +} + +static int allocate_for_block_cache(struct container_context *cntr, + struct block_header *header, void **buf) +{ + uint32_t block_size; + char *cache; + int err; + + if (header->type == BLOCK_TYPE_V110_DATA) + block_size = sizeof(struct block_v110_data); + else if (header->type == BLOCK_TYPE_CONTINUOUS_DATA) + block_size = sizeof(struct block_continuous_data); + else if (header->type == BLOCK_TYPE_EXTENDED_V110_FORMAT) + block_size = sizeof(struct block_extended_v110_format); + else if (header->type == BLOCK_TYPE_V120_DATA) + block_size = sizeof(struct block_v120_format); + else + block_size = parse_block_data_size(header->size); + + cache = malloc(block_size); + if (cache == NULL) + return -ENOMEM; + memset(cache, 0, block_size); + + memcpy(cache, header, sizeof(*header)); + err = container_recursive_read(cntr, cache + sizeof(*header), + block_size - sizeof(*header)); + if (err < 0) { + free(cache); + return err; + } + if (cntr->eof) { + free(cache); + return 0; + } + + *buf = cache; + + return 0; +} + +static int cache_data_block(struct container_context *cntr, + struct block_header *header, void **buf) +{ + int err; + + // Check type of this block. + err = container_recursive_read(cntr, &header->type, + sizeof(header->type)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + if (header->type > BLOCK_TYPE_V120_DATA) + return -EIO; + if (header->type == BLOCK_TYPE_TERMINATOR) + return 0; + + // Check size of this block. If the block includes a batch of data, + err = container_recursive_read(cntr, &header->size, + sizeof(header->size)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + return allocate_for_block_cache(cntr, header, buf); +} + +static int detect_format_block(struct container_context *cntr) +{ + struct parser_state *state = cntr->private_data; + struct block_header header; + void *buf; + int err; + +again: + buf = NULL; + err = cache_data_block(cntr, &header, &buf); + if (err < 0) + return err; + if (buf) { + if (header.type == BLOCK_TYPE_EXTENDED_V110_FORMAT) { + err = parse_extended_v110_format(state, buf); + } else if (header.type == BLOCK_TYPE_V120_DATA) { + err = parse_v120_format_block(state, buf); + } else if (header.type == BLOCK_TYPE_V110_DATA) { + err = parse_v110_data(state, buf); + } else { + free(buf); + goto again; + } + + free(buf); + + if (err < 0) + return err; + } + + // Expect to detect block_v110_data. + if (header.type == BLOCK_TYPE_EXTENDED_V110_FORMAT) + goto again; + + return 0; +} + +static int voc_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + unsigned int i; + int err; + + err = detect_container_version(cntr); + if (err < 0) + return err; + + err = detect_format_block(cntr); + if (err < 0) + return err; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].code_id == state->code_id) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + *format = format_maps[i].format; + *samples_per_frame = state->samples_per_frame; + *frames_per_second = state->frames_per_second; + + // This program handles PCM frames in this data block only. + *byte_count = state->byte_count; + + return 0; +} + +struct builder_state { + unsigned int version; + bool extended; + enum code_id code_id; + + unsigned int samples_per_frame; + unsigned int bytes_per_sample; +}; + +static int write_container_header(struct container_context *cntr, + struct container_header *header) +{ + struct builder_state *state = cntr->private_data; + + // Process container header. + memcpy(header->magic, VOC_MAGIC, sizeof(header->magic)); + header->hdr_size = htole16(sizeof(*header)); + header->version = htole16(state->version); + header->version_compr = htole16(0x1234 + ~state->version); + + return container_recursive_write(cntr, header, sizeof(*header)); +} + +static int write_v120_format_block(struct container_context *cntr, + struct block_v120_format *block, + unsigned int frames_per_second, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + + block->type = BLOCK_TYPE_V120_DATA; + build_block_data_size(block->size, 12 + byte_count); + + block->frames_per_second = htole32(frames_per_second); + block->bits_per_sample = state->bytes_per_sample * 8; + block->samples_per_frame = state->samples_per_frame; + block->code_id = htole16(state->code_id); + + return container_recursive_write(cntr, block, sizeof(*block)); +} + +static int write_extended_v110_format_block(struct container_context *cntr, + unsigned int frames_per_second, + struct block_extended_v110_format *block) +{ + struct builder_state *state = cntr->private_data; + uint16_t time_const; + int err; + + block->type = BLOCK_TYPE_EXTENDED_V110_FORMAT; + build_block_data_size(block->size, 4); + + // 16 bits are available for this purpose. + err = build_time_constant(frames_per_second, state->samples_per_frame, + &time_const, true); + if (err < 0) + return err; + block->time_const = htole16(time_const); + block->code_id = htole16(state->code_id); + + if (state->samples_per_frame == 1) + block->ch_mode = 0; + else + block->ch_mode = 1; + + return container_recursive_write(cntr, block, sizeof(*block)); +} + +static int write_v110_format_block(struct container_context *cntr, + struct block_v110_data *block, + unsigned int frames_per_second, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + uint16_t time_const; + int err; + + block->type = BLOCK_TYPE_V110_DATA; + build_block_data_size(block->size, 2 + byte_count); + + // These fields were obsoleted by extension. + err = build_time_constant(frames_per_second, 1, &time_const, false); + if (err < 0) + return err; + block->time_const = (uint8_t)time_const; + block->code_id = state->code_id; + return container_recursive_write(cntr, block, sizeof(*block)); +} + +static int write_data_blocks(struct container_context *cntr, + unsigned int frames_per_second, + uint64_t byte_count) +{ + union { + struct container_header header; + struct block_v110_data v110_data; + struct block_extended_v110_format extended_v110_format; + struct block_v120_format v120_format; + } buf = {0}; + struct builder_state *state = cntr->private_data; + int err; + + err = write_container_header(cntr, &buf.header); + if (err < 0) + return err; + + if (state->version == VOC_VERSION_1_20) { + err = write_v120_format_block(cntr, &buf.v120_format, + frames_per_second, byte_count); + } else { + if (state->extended) { + err = write_extended_v110_format_block(cntr, + frames_per_second, + &buf.extended_v110_format); + if (err < 0) + return err; + } + err = write_v110_format_block(cntr, &buf.v110_data, + frames_per_second, byte_count); + } + + return err; +} + +static int voc_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct builder_state *state = cntr->private_data; + unsigned int i; + + // Validate parameters. + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].format == *format) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + state->code_id = format_maps[i].code_id; + + // Decide container version. + if (*samples_per_frame > 2) + state->version = VOC_VERSION_1_20; + else + state->version = format_maps[i].minimal_version; + if (state->version == VOC_VERSION_1_10) { + if (*samples_per_frame == 2) { + for (i = 0; + i < ARRAY_SIZE(ex_v110_time_consts); ++i) { + if (ex_v110_time_consts[i].frames_per_second == + *frames_per_second) + break; + } + if (i == ARRAY_SIZE(ex_v110_time_consts)) + state->version = VOC_VERSION_1_20; + else + state->extended = true; + } else { + for (i = 0; i < ARRAY_SIZE(v110_time_consts); ++i) { + if (v110_time_consts[i].frames_per_second == + *frames_per_second) + break; + } + if (i == ARRAY_SIZE(v110_time_consts)) + state->version = VOC_VERSION_1_20; + } + } + + state->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + state->samples_per_frame = *samples_per_frame; + + return write_data_blocks(cntr, *frames_per_second, *byte_count); +} + +static int write_block_terminator(struct container_context *cntr) +{ + struct block_terminator block = {0}; + + block.type = BLOCK_TYPE_TERMINATOR; + return container_recursive_write(cntr, &block, sizeof(block)); +} + +static int write_data_size(struct container_context *cntr, uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + off_t offset; + uint8_t size_field[3]; + int err; + + offset = sizeof(struct container_header) + sizeof(uint8_t); + if (state->version == VOC_VERSION_1_10 && state->extended) + offset += sizeof(struct block_extended_v110_format); + err = container_seek_offset(cntr, offset); + if (err < 0) + return err; + + if (state->version == VOC_VERSION_1_10) + offset = 2; + else + offset = 12; + + if (byte_count > cntr->max_size - offset) + byte_count = cntr->max_size; + else + byte_count += offset; + build_block_data_size(size_field, byte_count); + + return container_recursive_write(cntr, &size_field, sizeof(size_field)); +} + +static int voc_builder_post_process(struct container_context *cntr, + uint64_t handled_byte_count) +{ + int err; + + err = write_block_terminator(cntr); + if (err < 0) + return err; + + return write_data_size(cntr, handled_byte_count); +} + +const struct container_parser container_parser_voc = { + .format = CONTAINER_FORMAT_VOC, + .magic = VOC_MAGIC, + .max_size = 0xffffff - // = UINT24_MAX. + sizeof(struct block_terminator), + .ops = { + .pre_process = voc_parser_pre_process, + }, + .private_size = sizeof(struct parser_state), +}; + +const struct container_builder container_builder_voc = { + .format = CONTAINER_FORMAT_VOC, + .max_size = 0xffffff - // = UINT24_MAX. + sizeof(struct block_terminator), + .ops = { + .pre_process = voc_builder_pre_process, + .post_process = voc_builder_post_process, + }, + .private_size = sizeof(struct builder_state), +}; diff --git a/axfer/container.c b/axfer/container.c new file mode 100644 index 0000000..a325fef --- /dev/null +++ b/axfer/container.c @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container.c - an interface of parser/builder for formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +#include +#include +#include +#include +#include + +static const char *const cntr_type_labels[] = { + [CONTAINER_TYPE_PARSER] = "parser", + [CONTAINER_TYPE_BUILDER] = "builder", +}; + +static const char *const cntr_format_labels[] = { + [CONTAINER_FORMAT_RIFF_WAVE] = "riff/wave", + [CONTAINER_FORMAT_AU] = "au", + [CONTAINER_FORMAT_VOC] = "voc", + [CONTAINER_FORMAT_RAW] = "raw", +}; + +static const char *const suffixes[] = { + [CONTAINER_FORMAT_RIFF_WAVE] = ".wav", + [CONTAINER_FORMAT_AU] = ".au", + [CONTAINER_FORMAT_VOC] = ".voc", + [CONTAINER_FORMAT_RAW] = "", +}; + +const char * container_suffix_from_format(enum container_format format) +{ + return suffixes[format]; +} + +int container_recursive_read(struct container_context *cntr, void *buf, + unsigned int byte_count) +{ + char *dst = buf; + ssize_t result; + size_t consumed = 0; + + while (consumed < byte_count && !cntr->interrupted) { + result = read(cntr->fd, dst + consumed, byte_count - consumed); + if (result < 0) { + // This descriptor was configured with non-blocking + // mode. EINTR is not cought when get any interrupts. + if (cntr->interrupted) + return -EINTR; + if (errno == EAGAIN) + continue; + return -errno; + } + // Reach EOF. + if (result == 0) { + cntr->eof = true; + return 0; + } + + consumed += result; + } + + return 0; +} + +int container_recursive_write(struct container_context *cntr, void *buf, + unsigned int byte_count) +{ + char *src = buf; + ssize_t result; + size_t consumed = 0; + + while (consumed < byte_count && !cntr->interrupted) { + result = write(cntr->fd, src + consumed, byte_count - consumed); + if (result < 0) { + // This descriptor was configured with non-blocking + // mode. EINTR is not cought when get any interrupts. + if (cntr->interrupted) + return -EINTR; + if (errno == EAGAIN) + continue; + return -errno; + } + + consumed += result; + } + + return 0; +} + +enum container_format container_format_from_path(const char *path) +{ + const char *suffix; + const char *pos; + int i; + + for (i = 0; i < (int)ARRAY_SIZE(suffixes); ++i) { + suffix = suffixes[i]; + + // Check last part of the string. + pos = path + strlen(path) - strlen(suffix); + if (!strcmp(pos, suffix)) + return i; + } + + // Unsupported. + return CONTAINER_FORMAT_RAW; +} + +int container_seek_offset(struct container_context *cntr, off_t offset) +{ + off_t pos; + + pos = lseek(cntr->fd, offset, SEEK_SET); + if (pos < 0) + return -errno; + if (pos != offset) + return -EIO; + + return 0; +} + +// To avoid blocking execution at system call iteration after receiving UNIX +// signals. +static int set_nonblock_flag(int fd) +{ + int flags; + + flags = fcntl(fd, F_GETFL); + if (flags < 0) + return -errno; + + flags |= O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) < 0) + return -errno; + + return 0; +} + +int container_parser_init(struct container_context *cntr, int fd, + unsigned int verbose) +{ + const struct container_parser *parsers[] = { + [CONTAINER_FORMAT_RIFF_WAVE] = &container_parser_riff_wave, + [CONTAINER_FORMAT_AU] = &container_parser_au, + [CONTAINER_FORMAT_VOC] = &container_parser_voc, + }; + const struct container_parser *parser; + unsigned int size; + int i; + int err; + + assert(cntr); + assert(fd >= 0); + + // Detect forgotten to destruct. + assert(cntr->fd == 0); + assert(cntr->private_data == NULL); + + memset(cntr, 0, sizeof(*cntr)); + + cntr->fd = fd; + + cntr->stdio = (cntr->fd == fileno(stdin)); + if (cntr->stdio) { + if (isatty(cntr->fd)) { + fprintf(stderr, + "A terminal is referred for standard input. " + "Output from any process or shell redirection " + "should be referred instead.\n"); + return -EIO; + } + } + + err = set_nonblock_flag(cntr->fd); + if (err < 0) + return err; + + // 4 bytes are enough to detect supported containers. + err = container_recursive_read(cntr, cntr->magic, sizeof(cntr->magic)); + if (err < 0) + return err; + for (i = 0; i < (int)ARRAY_SIZE(parsers); ++i) { + parser = parsers[i]; + size = strlen(parser->magic); + if (size > 4) + size = 4; + if (!strncmp(cntr->magic, parser->magic, size)) + break; + } + + // Don't forget that the first 4 bytes were already read for magic + // bytes. + cntr->magic_handled = false; + + // Unless detected, use raw container. + if (i == ARRAY_SIZE(parsers)) + parser = &container_parser_raw; + + // Allocate private data for the parser. + if (parser->private_size > 0) { + cntr->private_data = malloc(parser->private_size); + if (cntr->private_data == NULL) + return -ENOMEM; + memset(cntr->private_data, 0, parser->private_size); + } + + cntr->type = CONTAINER_TYPE_PARSER; + cntr->process_bytes = container_recursive_read; + cntr->format = parser->format; + cntr->ops = &parser->ops; + cntr->max_size = parser->max_size; + cntr->verbose = verbose; + + return 0; +} + +int container_builder_init(struct container_context *cntr, int fd, + enum container_format format, unsigned int verbose) +{ + const struct container_builder *builders[] = { + [CONTAINER_FORMAT_RIFF_WAVE] = &container_builder_riff_wave, + [CONTAINER_FORMAT_AU] = &container_builder_au, + [CONTAINER_FORMAT_VOC] = &container_builder_voc, + [CONTAINER_FORMAT_RAW] = &container_builder_raw, + }; + const struct container_builder *builder; + int err; + + assert(cntr); + assert(fd >= 0); + + // Detect forgotten to destruct. + assert(cntr->fd == 0); + assert(cntr->private_data == NULL); + + memset(cntr, 0, sizeof(*cntr)); + + cntr->fd = fd; + + cntr->stdio = (cntr->fd == fileno(stdout)); + if (cntr->stdio) { + if (isatty(cntr->fd)) { + fprintf(stderr, + "A terminal is referred for standard output. " + "Input to any process or shell redirection " + "should be referred instead.\n"); + return -EIO; + } + } + + err = set_nonblock_flag(cntr->fd); + if (err < 0) + return err; + + builder = builders[format]; + + // Allocate private data for the builder. + if (builder->private_size > 0) { + cntr->private_data = malloc(builder->private_size); + if (cntr->private_data == NULL) + return -ENOMEM; + memset(cntr->private_data, 0, builder->private_size); + } + + cntr->type = CONTAINER_TYPE_BUILDER; + cntr->process_bytes = container_recursive_write; + cntr->format = builder->format; + cntr->ops = &builder->ops; + cntr->max_size = builder->max_size; + cntr->verbose = verbose; + + return 0; +} + +int container_context_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *frame_count) +{ + uint64_t byte_count = 0; + unsigned int bytes_per_frame; + int err; + + assert(cntr); + assert(format); + assert(samples_per_frame); + assert(frames_per_second); + assert(frame_count); + + if (cntr->type == CONTAINER_TYPE_BUILDER) + byte_count = cntr->max_size; + + if (cntr->ops->pre_process) { + err = cntr->ops->pre_process(cntr, format, samples_per_frame, + frames_per_second, &byte_count); + if (err < 0) + return err; + if (cntr->eof) + return 0; + } + + if (cntr->format == CONTAINER_FORMAT_RAW) { + if (*format == SND_PCM_FORMAT_UNKNOWN || + *samples_per_frame == 0 || *frames_per_second == 0) { + fprintf(stderr, + "Any file format is not detected. Need to " + "indicate all of sample format, channels and " + "rate explicitly.\n"); + return -EINVAL; + } + } + assert(*format >= SND_PCM_FORMAT_S8); + assert(*format <= SND_PCM_FORMAT_LAST); + assert(*samples_per_frame > 0); + assert(*frames_per_second > 0); + assert(byte_count > 0); + + cntr->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + cntr->samples_per_frame = *samples_per_frame; + cntr->frames_per_second = *frames_per_second; + + bytes_per_frame = cntr->bytes_per_sample * *samples_per_frame; + *frame_count = byte_count / bytes_per_frame; + cntr->max_size -= cntr->max_size / bytes_per_frame; + + if (cntr->verbose > 0) { + fprintf(stderr, "Container: %s\n", + cntr_type_labels[cntr->type]); + fprintf(stderr, " format: %s\n", + cntr_format_labels[cntr->format]); + fprintf(stderr, " sample format: %s\n", + snd_pcm_format_name(*format)); + fprintf(stderr, " bytes/sample: %u\n", + cntr->bytes_per_sample); + fprintf(stderr, " samples/frame: %u\n", + cntr->samples_per_frame); + fprintf(stderr, " frames/second: %u\n", + cntr->frames_per_second); + if (cntr->type == CONTAINER_TYPE_PARSER) { + fprintf(stderr, " frames: %" PRIu64 "\n", + *frame_count); + } else { + fprintf(stderr, " max frames: %" PRIu64 "\n", + *frame_count); + } + } + + return 0; +} + +int container_context_process_frames(struct container_context *cntr, + void *frame_buffer, + unsigned int *frame_count) +{ + char *buf = frame_buffer; + unsigned int bytes_per_frame; + unsigned int byte_count; + unsigned int target_byte_count; + int err; + + assert(cntr); + assert(!cntr->eof); + assert(frame_buffer); + assert(frame_count); + + bytes_per_frame = cntr->bytes_per_sample * cntr->samples_per_frame; + target_byte_count = *frame_count * bytes_per_frame; + + // A parser of cotainers already read first 4 bytes to detect format + // of container, however they includes PCM frames when any format was + // undetected. Surely to write out them. + byte_count = target_byte_count; + if (cntr->format == CONTAINER_FORMAT_RAW && + cntr->type == CONTAINER_TYPE_PARSER && !cntr->magic_handled) { + memcpy(buf, cntr->magic, sizeof(cntr->magic)); + buf += sizeof(cntr->magic); + byte_count -= sizeof(cntr->magic); + cntr->magic_handled = true; + } + + // Each container has limitation for its volume for sample data. + if (cntr->handled_byte_count > cntr->max_size - byte_count) + byte_count = cntr->max_size - cntr->handled_byte_count; + + // All of supported containers include interleaved PCM frames. + // TODO: process frames for truncate case. + err = cntr->process_bytes(cntr, buf, byte_count); + if (err < 0) { + *frame_count = 0; + return err; + } + + cntr->handled_byte_count += target_byte_count; + if (cntr->handled_byte_count == cntr->max_size) + cntr->eof = true; + + *frame_count = target_byte_count / bytes_per_frame; + + return 0; +} + +int container_context_post_process(struct container_context *cntr, + uint64_t *frame_count) +{ + int err = 0; + + assert(cntr); + assert(frame_count); + + if (cntr->verbose && cntr->handled_byte_count > 0) { + fprintf(stderr, " Handled bytes: %" PRIu64 "\n", + cntr->handled_byte_count); + } + + // NOTE* we cannot seek when using standard input/output. + if (!cntr->stdio && cntr->ops && cntr->ops->post_process) { + // Usually, need to write out processed bytes in container + // header even it this program is interrupted. + cntr->interrupted = false; + + err = cntr->ops->post_process(cntr, cntr->handled_byte_count); + } + + // Ensure to perform write-back from disk cache. + if (cntr->type == CONTAINER_TYPE_BUILDER) + fsync(cntr->fd); + + if (err < 0) + return err; + + if (cntr->bytes_per_sample == 0 || cntr->samples_per_frame == 0) { + *frame_count = 0; + } else { + *frame_count = cntr->handled_byte_count / + cntr->bytes_per_sample / + cntr->samples_per_frame; + } + + return 0; +} + +void container_context_destroy(struct container_context *cntr) +{ + assert(cntr); + + if (cntr->private_data) + free(cntr->private_data); + + cntr->fd = 0; + cntr->private_data = NULL; +} diff --git a/axfer/container.h b/axfer/container.h new file mode 100644 index 0000000..3dd466a --- /dev/null +++ b/axfer/container.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container.h - an interface of parser/builder for formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_CONTAINER__H_ +#define __ALSA_UTILS_AXFER_CONTAINER__H_ + +#include "aconfig.h" + +#define _LARGEFILE64_SOURCE +#include + +#include +#include + +#include + +#include "os_compat.h" + +enum container_type { + CONTAINER_TYPE_PARSER = 0, + CONTAINER_TYPE_BUILDER, + CONTAINER_TYPE_COUNT, +}; + +enum container_format { + CONTAINER_FORMAT_RIFF_WAVE = 0, + CONTAINER_FORMAT_AU, + CONTAINER_FORMAT_VOC, + CONTAINER_FORMAT_RAW, + CONTAINER_FORMAT_COUNT, +}; + +struct container_ops; + +struct container_context { + enum container_type type; + int fd; + int (*process_bytes)(struct container_context *cntr, + void *buffer, unsigned int byte_count); + bool magic_handled; + bool eof; + bool interrupted; + bool stdio; + + enum container_format format; + uint64_t max_size; + char magic[4]; + const struct container_ops *ops; + void *private_data; + + // Available after pre-process. + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_second; + + unsigned int verbose; + uint64_t handled_byte_count; +}; + +const char *container_suffix_from_format(enum container_format format); +enum container_format container_format_from_path(const char *path); +int container_parser_init(struct container_context *cntr, int fd, + unsigned int verbose); +int container_builder_init(struct container_context *cntr, int fd, + enum container_format format, unsigned int verbose); +void container_context_destroy(struct container_context *cntr); +int container_context_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *frame_count); +int container_context_process_frames(struct container_context *cntr, + void *frame_buffer, + unsigned int *frame_count); +int container_context_post_process(struct container_context *cntr, + uint64_t *frame_count); + +// For internal use in 'container' module. + +struct container_ops { + int (*pre_process)(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count); + int (*post_process)(struct container_context *cntr, + uint64_t handled_byte_count); +}; +struct container_parser { + enum container_format format; + const char *const magic; + uint64_t max_size; + struct container_ops ops; + unsigned int private_size; +}; + +struct container_builder { + enum container_format format; + const char *const suffix; + uint64_t max_size; + struct container_ops ops; + unsigned int private_size; +}; + +int container_recursive_read(struct container_context *cntr, void *buf, + unsigned int byte_count); +int container_recursive_write(struct container_context *cntr, void *buf, + unsigned int byte_count); +int container_seek_offset(struct container_context *cntr, off_t offset); + +extern const struct container_parser container_parser_riff_wave; +extern const struct container_builder container_builder_riff_wave; + +extern const struct container_parser container_parser_au; +extern const struct container_builder container_builder_au; + +extern const struct container_parser container_parser_voc; +extern const struct container_builder container_builder_voc; + +extern const struct container_parser container_parser_raw; +extern const struct container_builder container_builder_raw; + +#endif diff --git a/axfer/frame-cache.c b/axfer/frame-cache.c new file mode 100644 index 0000000..8538f1e --- /dev/null +++ b/axfer/frame-cache.c @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// frame-cache.c - maintainer of cache for data frame. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "frame-cache.h" + +static void align_frames_in_i(struct frame_cache *cache, + unsigned int consumed_count) +{ + char *buf = cache->buf; + unsigned int offset; + unsigned int size; + + cache->remained_count -= consumed_count; + + offset = cache->bytes_per_sample * cache->samples_per_frame * + consumed_count; + size = cache->bytes_per_sample * cache->samples_per_frame * + cache->remained_count; + memmove(buf, buf + offset, size); + + cache->buf_ptr = buf + size; +} + +static void align_frames_in_n(struct frame_cache *cache, + unsigned int consumed_count) +{ + char **bufs = cache->buf; + char **buf_ptrs = cache->buf_ptr; + unsigned int offset; + unsigned int size; + unsigned int i; + + cache->remained_count -= consumed_count; + + for (i = 0; i < cache->samples_per_frame; ++i) { + offset = cache->bytes_per_sample * consumed_count; + size = cache->bytes_per_sample * cache->remained_count; + memmove(bufs[i], bufs[i] + offset, size); + buf_ptrs[i] = bufs[i] + size; + } +} + +int frame_cache_init(struct frame_cache *cache, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_cache) +{ + cache->access = access; + cache->remained_count = 0; + cache->bytes_per_sample = bytes_per_sample; + cache->samples_per_frame = samples_per_frame; + cache->frames_per_cache = frames_per_cache; + + if (access == SND_PCM_ACCESS_RW_INTERLEAVED) + cache->align_frames = align_frames_in_i; + else if (access == SND_PCM_ACCESS_RW_NONINTERLEAVED) + cache->align_frames = align_frames_in_n; + else + return -EINVAL; + + if (access == SND_PCM_ACCESS_RW_INTERLEAVED) { + char *buf; + + buf = calloc(frames_per_cache, + bytes_per_sample * samples_per_frame); + if (buf == NULL) + goto nomem; + cache->buf = buf; + cache->buf_ptr = buf; + } else { + char **bufs = calloc(samples_per_frame, sizeof(*bufs)); + char **buf_ptrs = calloc(samples_per_frame, sizeof(*buf_ptrs)); + unsigned int i; + + cache->buf = bufs; + cache->buf_ptr = buf_ptrs; + if (bufs == NULL || buf_ptrs == NULL) + goto nomem; + for (i = 0; i < samples_per_frame; ++i) { + bufs[i] = calloc(frames_per_cache, bytes_per_sample); + if (bufs[i] == NULL) + goto nomem; + buf_ptrs[i] = bufs[i]; + } + } + + + return 0; + +nomem: + frame_cache_destroy(cache); + return -ENOMEM; +} + +void frame_cache_destroy(struct frame_cache *cache) +{ + if (cache->access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { + char **bufs = cache->buf; + if (bufs) { + unsigned int i; + for (i = 0; i < cache->samples_per_frame; ++i) + free(bufs[i]); + } + free(cache->buf_ptr); + } + free(cache->buf); + memset(cache, 0, sizeof(*cache)); +} diff --git a/axfer/frame-cache.h b/axfer/frame-cache.h new file mode 100644 index 0000000..7191333 --- /dev/null +++ b/axfer/frame-cache.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// frame-cache.h - maintainer of cache for data frame. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include + +struct frame_cache { + void *buf; + void *buf_ptr; + + unsigned int remained_count; + + snd_pcm_access_t access; + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_cache; + + void (*align_frames)(struct frame_cache *cache, + unsigned int consumed_count); +}; + +int frame_cache_init(struct frame_cache *cache, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_cache); +void frame_cache_destroy(struct frame_cache *cache); + +static inline unsigned int frame_cache_get_count(struct frame_cache *cache) +{ + return cache->remained_count; +} + +static inline void frame_cache_increase_count(struct frame_cache *cache, + unsigned int frame_count) +{ + cache->remained_count += frame_count; +} + +static inline void frame_cache_reduce(struct frame_cache *cache, + unsigned int consumed_count) +{ + cache->align_frames(cache, consumed_count); +} diff --git a/axfer/main.c b/axfer/main.c new file mode 100644 index 0000000..765d2c3 --- /dev/null +++ b/axfer/main.c @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-2.0 +// main.c - an entry point for this program. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Originally written as 'aplay', by Michael Beck and Jaroslav Kysela. +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "subcmd.h" +#include "misc.h" + +#include "version.h" + +#include +#include +#include +#include + +enum subcmds { + SUBCMD_TRANSFER = 0, + SUBCMD_LIST, + SUBCMD_HELP, + SUBCMD_VERSION, +}; + +char *arg_duplicate_string(const char *str, int *err) +{ + char *ptr; + + // For safe. + if (strlen(str) > 1024) { + *err = -EINVAL; + return NULL; + } + + ptr = strdup(str); + if (ptr == NULL) + *err = -ENOMEM; + + return ptr; +} + +long arg_parse_decimal_num(const char *str, int *err) +{ + long val; + char *endptr; + + errno = 0; + val = strtol(str, &endptr, 0); + if (errno > 0) { + *err = -errno; + return 0; + } + if (*endptr != '\0') { + *err = -EINVAL; + return 0; + } + + return val; +} + +static void print_version(const char *const cmdname) +{ + printf("%s: version %s\n", cmdname, SND_UTIL_VERSION_STR); +} + +static void print_help(void) +{ + printf( +"Usage:\n" +" axfer transfer DIRECTION OPTIONS\n" +" axfer list DIRECTION OPTIONS\n" +" axfer version\n" +" axfer help\n" +"\n" +" where:\n" +" DIRECTION = capture | playback\n" +" OPTIONS = -h | --help | (subcommand specific)\n" + ); +} + +// Backward compatibility to aplay(1). +static bool decide_subcmd(int argc, char *const *argv, enum subcmds *subcmd) +{ + static const struct { + const char *const name; + enum subcmds subcmd; + } long_opts[] = { + {"--list-devices", SUBCMD_LIST}, + {"--list-pcms", SUBCMD_LIST}, + {"--help", SUBCMD_HELP}, + {"--version", SUBCMD_VERSION}, + }; + static const struct { + unsigned char c; + enum subcmds subcmd; + } short_opts[] = { + {'l', SUBCMD_LIST}, + {'L', SUBCMD_LIST}, + {'h', SUBCMD_HELP}, + }; + char *pos; + int i, j; + + if (argc == 1) + return false; + + // Original command system. For long options. + for (i = 0; i < (int)ARRAY_SIZE(long_opts); ++i) { + for (j = 0; j < argc; ++j) { + if (!strcmp(long_opts[i].name, argv[j])) { + *subcmd = long_opts[i].subcmd; + return true; + } + } + } + + // Original command system. For short options. + for (i = 1; i < argc; ++i) { + // Pick up short options only. + if (argv[i][0] != '-' || argv[i][0] == '\0' || + argv[i][1] == '-' || argv[i][1] == '\0') + continue; + for (pos = argv[i]; *pos != '\0'; ++pos) { + for (j = 0; j < (int)ARRAY_SIZE(short_opts); ++j) { + if (*pos == short_opts[j].c) { + *subcmd = short_opts[j].subcmd; + return true; + } + } + } + } + + return false; +} + +// Backward compatibility to aplay(1). +static bool decide_direction(int argc, char *const *argv, + snd_pcm_stream_t *direction) +{ + static const struct { + const char *const name; + snd_pcm_stream_t direction; + } long_opts[] = { + {"--capture", SND_PCM_STREAM_CAPTURE}, + {"--playback", SND_PCM_STREAM_PLAYBACK}, + }; + static const struct { + unsigned char c; + snd_pcm_stream_t direction; + } short_opts[] = { + {'C', SND_PCM_STREAM_CAPTURE}, + {'P', SND_PCM_STREAM_PLAYBACK}, + }; + static const char *const aliases[] = { + [SND_PCM_STREAM_CAPTURE] = "arecord", + [SND_PCM_STREAM_PLAYBACK] = "aplay", + }; + int i, j; + char *pos; + + // Original command system. For long options. + for (i = 0; i < (int)ARRAY_SIZE(long_opts); ++i) { + for (j = 0; j < argc; ++j) { + if (!strcmp(long_opts[i].name, argv[j])) { + *direction = long_opts[i].direction; + return true; + } + } + } + + // Original command system. For short options. + for (i = 1; i < argc; ++i) { + // Pick up short options only. + if (argv[i][0] != '-' || argv[i][0] == '\0' || + argv[i][1] == '-' || argv[i][1] == '\0') + continue; + for (pos = argv[i]; *pos != '\0'; ++pos) { + for (j = 0; j < (int)ARRAY_SIZE(short_opts); ++j) { + if (*pos == short_opts[j].c) { + *direction = short_opts[j].direction; + return true; + } + } + } + } + + // If not decided yet, judge according to command name. + for (i = 0; i < (int)ARRAY_SIZE(aliases); ++i) { + for (pos = argv[0] + strlen(argv[0]); pos != argv[0]; --pos) { + if (strstr(pos, aliases[i]) != NULL) { + *direction = i; + return true; + } + } + } + + return false; +} + +static bool detect_subcmd(int argc, char *const *argv, enum subcmds *subcmd) +{ + static const char *const subcmds[] = { + [SUBCMD_TRANSFER] = "transfer", + [SUBCMD_LIST] = "list", + [SUBCMD_HELP] = "help", + [SUBCMD_VERSION] = "version", + }; + int i; + + if (argc < 2) + return false; + + for (i = 0; i < (int)ARRAY_SIZE(subcmds); ++i) { + if (!strcmp(argv[1], subcmds[i])) { + *subcmd = i; + return true; + } + } + + return false; +} + +static bool detect_direction(int argc, char *const *argv, + snd_pcm_stream_t *direction) +{ + if (argc < 3) + return false; + + if (!strcmp(argv[2], "capture")) { + *direction = SND_PCM_STREAM_CAPTURE; + return true; + } + + if (!strcmp(argv[2], "playback")) { + *direction = SND_PCM_STREAM_PLAYBACK; + return true; + } + + return false; +} + +int main(int argc, char *const *argv) +{ + snd_pcm_stream_t direction; + enum subcmds subcmd; + int err = 0; + + // For compatibility to aplay(1) implementation. + if (strstr(argv[0], "arecord") == argv[0] + strlen(argv[0]) - 7 || + strstr(argv[0], "aplay") == argv[0] + strlen(argv[0]) - 5) { + if (!decide_direction(argc, argv, &direction)) + direction = SND_PCM_STREAM_PLAYBACK; + if (!decide_subcmd(argc, argv, &subcmd)) + subcmd = SUBCMD_TRANSFER; + } else { + // The first option should be one of subcommands. + if (!detect_subcmd(argc, argv, &subcmd)) + subcmd = SUBCMD_HELP; + // The second option should be either 'capture' or 'direction' + // if subcommand is neither 'version' nor 'help'. + if (subcmd != SUBCMD_VERSION && subcmd != SUBCMD_HELP) { + if (!detect_direction(argc, argv, &direction)) { + subcmd = SUBCMD_HELP; + } else { + // argv[0] is needed for unparsed option to use + // getopt_long(3). + argc -= 2; + argv += 2; + } + } + } + + if (subcmd == SUBCMD_TRANSFER) + err = subcmd_transfer(argc, argv, direction); + else if (subcmd == SUBCMD_LIST) + err = subcmd_list(argc, argv, direction); + else if (subcmd == SUBCMD_VERSION) + print_version(argv[0]); + else + print_help(); + if (err < 0) + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} diff --git a/axfer/mapper-multiple.c b/axfer/mapper-multiple.c new file mode 100644 index 0000000..8e82b22 --- /dev/null +++ b/axfer/mapper-multiple.c @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper-multiple.c - a muxer/demuxer for multiple containers. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "mapper.h" +#include "misc.h" + +struct multiple_state { + void (*align_frames)(void *frame_buf, unsigned int frame_count, + char **buf, unsigned int bytes_per_sample, + struct container_context *cntrs, + unsigned int cntr_count); + char **bufs; + unsigned int cntr_count; +}; + +static void align_to_i(void *frame_buf, unsigned int frame_count, + char **src_bufs, unsigned int bytes_per_sample, + struct container_context *cntrs, unsigned int cntr_count) +{ + char *dst = frame_buf; + char *src; + unsigned int dst_pos; + unsigned int src_pos; + struct container_context *cntr; + unsigned int i, j; + + // src: first channel in each of interleaved buffers in containers => + // dst:interleaved. + for (i = 0; i < cntr_count; ++i) { + src = src_bufs[i]; + cntr = cntrs + i; + + for (j = 0; j < frame_count; ++j) { + // Use first src channel for each of dst channel. + src_pos = bytes_per_sample * cntr->samples_per_frame * j; + dst_pos = bytes_per_sample * (cntr_count * j + i); + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static void align_from_i(void *frame_buf, unsigned int frame_count, + char **dst_bufs, unsigned int bytes_per_sample, + struct container_context *cntrs, + unsigned int cntr_count) +{ + char *src = frame_buf; + char *dst; + unsigned int src_pos; + unsigned int dst_pos; + struct container_context *cntr; + unsigned int i, j; + + for (i = 0; i < cntr_count; ++i) { + dst = dst_bufs[i]; + cntr = cntrs + i; + + for (j = 0; j < frame_count; ++j) { + // Use first src channel for each of dst channel. + src_pos = bytes_per_sample * (cntr_count * j + i); + dst_pos = bytes_per_sample * cntr->samples_per_frame * j; + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static int multiple_pre_process(struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct multiple_state *state = mapper->private_data; + struct container_context *cntr; + unsigned int i; + + // Additionally, format of samples in the containers should be the same + // as the format in PCM substream. + for (i = 0; i < cntr_count; ++i) { + cntr = cntrs + i; + if (mapper->bytes_per_sample != cntr->bytes_per_sample) + return -EINVAL; + } + state->cntr_count = cntr_count; + + // Decide method to align frames. + if (mapper->type == MAPPER_TYPE_DEMUXER) { + if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = align_from_i; + else if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } else { + if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = align_to_i; + else if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } + + if (state->align_frames) { + // Furthermore, in demuxer case, each container should be + // configured to store one sample per frame. + if (mapper->type == MAPPER_TYPE_DEMUXER) { + for (i = 0; i < cntr_count; ++i) { + cntr = cntrs + i; + if (cntr->samples_per_frame != 1) + return -EINVAL; + } + } + + state->bufs = calloc(cntr_count, sizeof(char *)); + if (state->bufs == NULL) + return -ENOMEM; + + for (i = 0; i < cntr_count; ++i) { + unsigned int bytes_per_buffer; + + // Allocate intermediate buffer as the same size as a + // period for each of containers. + cntr = cntrs + i; + + bytes_per_buffer = mapper->bytes_per_sample * + cntr->samples_per_frame * + mapper->frames_per_buffer; + + state->bufs[i] = malloc(bytes_per_buffer); + if (state->bufs[i] == NULL) + return -ENOMEM; + memset(state->bufs[i], 0, bytes_per_buffer); + } + } + + return 0; +} + +static int process_containers(char **src_bufs, unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct container_context *cntr; + char *src; + unsigned int i; + int err = 0; + + // TODO: arrangement for *frame_count. + for (i = 0; i < cntr_count; ++i) { + cntr = &cntrs[i]; + src = src_bufs[i]; + + err = container_context_process_frames(cntr, src, frame_count); + if (err < 0) + break; + } + + return err; +} + +static int multiple_muxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct multiple_state *state = mapper->private_data; + char **src_bufs; + int err; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + src_bufs = frame_buf; + } else { + src_bufs = state->bufs; + } + err = process_containers(src_bufs, frame_count, cntrs, cntr_count); + if (err < 0) + return err; + + // Unlikely. + if (src_bufs != frame_buf && *frame_count > 0) { + state->align_frames(frame_buf, *frame_count, src_bufs, + mapper->bytes_per_sample, cntrs, + cntr_count); + } + + return 0; +} + +static int multiple_demuxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct multiple_state *state = mapper->private_data; + char **dst_bufs; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + dst_bufs = frame_buf; + } else { + dst_bufs = state->bufs; + state->align_frames(frame_buf, *frame_count, dst_bufs, + mapper->bytes_per_sample, cntrs, + cntr_count); + } + + return process_containers(dst_bufs, frame_count, cntrs, cntr_count); +} + +static void multiple_post_process(struct mapper_context *mapper) +{ + struct multiple_state *state = mapper->private_data; + unsigned int i; + + if (state->bufs) { + for (i = 0; i < state->cntr_count; ++i) { + if (state->bufs[i]) + free(state->bufs[i]); + } + free(state->bufs); + } + + state->bufs = NULL; + state->align_frames = NULL; +} + +const struct mapper_data mapper_muxer_multiple = { + .ops = { + .pre_process = multiple_pre_process, + .process_frames = multiple_muxer_process_frames, + .post_process = multiple_post_process, + }, + .private_size = sizeof(struct multiple_state), +}; + +const struct mapper_data mapper_demuxer_multiple = { + .ops = { + .pre_process = multiple_pre_process, + .process_frames = multiple_demuxer_process_frames, + .post_process = multiple_post_process, + }, + .private_size = sizeof(struct multiple_state), +}; diff --git a/axfer/mapper-single.c b/axfer/mapper-single.c new file mode 100644 index 0000000..f669f7f --- /dev/null +++ b/axfer/mapper-single.c @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper-single.c - a muxer/demuxer for single containers. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "mapper.h" +#include "misc.h" + +struct single_state { + void (*align_frames)(void *frame_buf, unsigned int frame_count, + char *buf, unsigned int bytes_per_sample, + unsigned int samples_per_frame); + char *buf; +}; + +static void align_to_vector(void *frame_buf, unsigned int frame_count, + char *src, unsigned int bytes_per_sample, + unsigned samples_per_frame) +{ + char **dst_bufs = frame_buf; + char *dst; + unsigned int src_pos; + unsigned int dst_pos; + unsigned int i, j; + + // src: interleaved => dst: a set of interleaved buffers. + for (i = 0; i < samples_per_frame; ++i) { + dst = dst_bufs[i]; + for (j = 0; j < frame_count; ++j) { + src_pos = bytes_per_sample * (samples_per_frame * j + i); + dst_pos = bytes_per_sample * j; + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static void align_from_vector(void *frame_buf, unsigned int frame_count, + char *dst, unsigned int bytes_per_sample, + unsigned int samples_per_frame) +{ + char **src_bufs = frame_buf; + char *src; + unsigned int dst_pos; + unsigned int src_pos; + unsigned int i, j; + + // src: a set of interleaved buffers => dst:interleaved. + for (i = 0; i < samples_per_frame; ++i) { + src = src_bufs[i]; + for (j = 0; j < frame_count; ++j) { + src_pos = bytes_per_sample * j; + dst_pos = bytes_per_sample * (samples_per_frame * j + i); + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static int single_pre_process(struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int cntr_count ATTRIBUTE_UNUSED) +{ + struct single_state *state = mapper->private_data; + unsigned int bytes_per_buffer; + + if (cntrs->bytes_per_sample != mapper->bytes_per_sample || + cntrs->samples_per_frame != mapper->samples_per_frame) + return -EINVAL; + + // Decide method to align frames. + if (mapper->type == MAPPER_TYPE_DEMUXER) { + if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = align_from_vector; + else if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } else { + if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = align_to_vector; + else if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } + + if (state->align_frames) { + // Allocate intermediate buffer as the same size as a period. + bytes_per_buffer = mapper->bytes_per_sample * + mapper->samples_per_frame * + mapper->frames_per_buffer; + state->buf = malloc(bytes_per_buffer); + if (state->buf == NULL) + return -ENOMEM; + memset(state->buf, 0, bytes_per_buffer); + } + + return 0; +} + +static int single_muxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count ATTRIBUTE_UNUSED) +{ + struct single_state *state = mapper->private_data; + void *src; + int err; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + src = frame_buf; + } else { + src = state->buf; + } + err = container_context_process_frames(cntrs, src, frame_count); + if (err < 0) + return err; + + // Unlikely. + if (src != frame_buf && *frame_count > 0) + state->align_frames(frame_buf, *frame_count, src, + mapper->bytes_per_sample, + mapper->samples_per_frame); + + return 0; +} + +static int single_demuxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count ATTRIBUTE_UNUSED) +{ + struct single_state *state = mapper->private_data; + void *dst; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + dst = frame_buf; + } else { + state->align_frames(frame_buf, *frame_count, state->buf, + mapper->bytes_per_sample, + mapper->samples_per_frame); + dst = state->buf; + } + + return container_context_process_frames(cntrs, dst, frame_count); +} + +static void single_post_process(struct mapper_context *mapper) +{ + struct single_state *state = mapper->private_data; + + if (state->buf) + free(state->buf); + + state->buf = NULL; + state->align_frames = NULL; +} + +const struct mapper_data mapper_muxer_single = { + .ops = { + .pre_process = single_pre_process, + .process_frames = single_muxer_process_frames, + .post_process = single_post_process, + }, + .private_size = sizeof(struct single_state), +}; + +const struct mapper_data mapper_demuxer_single = { + .ops = { + .pre_process = single_pre_process, + .process_frames = single_demuxer_process_frames, + .post_process = single_post_process, + }, + .private_size = sizeof(struct single_state), +}; diff --git a/axfer/mapper.c b/axfer/mapper.c new file mode 100644 index 0000000..4c3f0e3 --- /dev/null +++ b/axfer/mapper.c @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper.c - an interface of muxer/demuxer between buffer with data frames and +// formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "mapper.h" +#include "misc.h" + +#include + +static const char *const mapper_type_labels[] = { + [MAPPER_TYPE_MUXER] = "muxer", + [MAPPER_TYPE_DEMUXER] = "demuxer", +}; + +static const char *const mapper_target_labels[] = { + [MAPPER_TARGET_SINGLE] = "single", + [MAPPER_TARGET_MULTIPLE] = "multiple", +}; + +int mapper_context_init(struct mapper_context *mapper, + enum mapper_type type, unsigned int cntr_count, + unsigned int verbose) +{ + const struct mapper_data *data = NULL; + + assert(mapper); + assert(cntr_count > 0); + + // Detect forgotten to destruct. + assert(mapper->private_data == NULL); + + memset(mapper, 0, sizeof(*mapper)); + + if (type == MAPPER_TYPE_MUXER) { + if (cntr_count == 1) { + data = &mapper_muxer_single; + mapper->target = MAPPER_TARGET_SINGLE; + } else { + data = &mapper_muxer_multiple; + mapper->target = MAPPER_TARGET_MULTIPLE; + } + } else { + if (cntr_count == 1) { + data = &mapper_demuxer_single; + mapper->target = MAPPER_TARGET_SINGLE; + } else { + data = &mapper_demuxer_multiple; + mapper->target = MAPPER_TARGET_MULTIPLE; + } + } + + mapper->ops = &data->ops; + mapper->type = type; + + mapper->private_data = malloc(data->private_size); + if (mapper->private_data == NULL) + return -ENOMEM; + memset(mapper->private_data, 0, data->private_size); + + mapper->cntr_count = cntr_count; + mapper->verbose = verbose; + + return 0; +} + +int mapper_context_pre_process(struct mapper_context *mapper, + snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + struct container_context *cntrs) +{ + int err; + + assert(mapper); + assert(access >= SND_PCM_ACCESS_MMAP_INTERLEAVED); + assert(access <= SND_PCM_ACCESS_RW_NONINTERLEAVED); + assert(bytes_per_sample > 0); + assert(samples_per_frame > 0); + assert(cntrs); + + // The purpose of multiple target is to mux/demux each channels to/from + // containers. + if (mapper->target == MAPPER_TARGET_MULTIPLE && + samples_per_frame != mapper->cntr_count) + return -EINVAL; + + mapper->access = access; + mapper->bytes_per_sample = bytes_per_sample; + mapper->samples_per_frame = samples_per_frame; + mapper->frames_per_buffer = frames_per_buffer; + + err = mapper->ops->pre_process(mapper, cntrs, mapper->cntr_count); + if (err < 0) + return err; + + if (mapper->verbose > 0) { + fprintf(stderr, "Mapper: %s\n", + mapper_type_labels[mapper->type]); + fprintf(stderr, " target: %s\n", + mapper_target_labels[mapper->target]); + fprintf(stderr, " access: %s\n", + snd_pcm_access_name(mapper->access)); + fprintf(stderr, " bytes/sample: %u\n", + mapper->bytes_per_sample); + fprintf(stderr, " samples/frame: %u\n", + mapper->samples_per_frame); + fprintf(stderr, " frames/buffer: %lu\n", + mapper->frames_per_buffer); + } + + return 0; +} + +int mapper_context_process_frames(struct mapper_context *mapper, + void *frame_buffer, + unsigned int *frame_count, + struct container_context *cntrs) +{ + assert(mapper); + assert(frame_buffer); + assert(frame_count); + assert(*frame_count <= mapper->frames_per_buffer); + assert(cntrs); + + return mapper->ops->process_frames(mapper, frame_buffer, frame_count, + cntrs, mapper->cntr_count); +} + +void mapper_context_post_process(struct mapper_context *mapper) +{ + assert(mapper); + + if (mapper->ops && mapper->ops->post_process) + mapper->ops->post_process(mapper); +} + +void mapper_context_destroy(struct mapper_context *mapper) +{ + assert(mapper); + + if (mapper->private_data) + free(mapper->private_data); + mapper->private_data = NULL; +} diff --git a/axfer/mapper.h b/axfer/mapper.h new file mode 100644 index 0000000..58b6118 --- /dev/null +++ b/axfer/mapper.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper.h - an interface of muxer/demuxer between buffer with data frames and +// formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_MAPPER__H_ +#define __ALSA_UTILS_AXFER_MAPPER__H_ + +#include "container.h" + +enum mapper_type { + MAPPER_TYPE_MUXER = 0, + MAPPER_TYPE_DEMUXER, + MAPPER_TYPE_COUNT, +}; + +enum mapper_target { + MAPPER_TARGET_SINGLE = 0, + MAPPER_TARGET_MULTIPLE, + MAPPER_TARGET_COUNT, +}; + +struct mapper_ops; + +struct mapper_context { + enum mapper_type type; + enum mapper_target target; + const struct mapper_ops *ops; + unsigned int private_size; + + void *private_data; + unsigned int cntr_count; + + // A part of parameters of PCM substream. + snd_pcm_access_t access; + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + snd_pcm_uframes_t frames_per_buffer; + + unsigned int verbose; +}; + +int mapper_context_init(struct mapper_context *mapper, + enum mapper_type type, unsigned int cntr_count, + unsigned int verbose); +int mapper_context_pre_process(struct mapper_context *mapper, + snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + struct container_context *cntrs); +int mapper_context_process_frames(struct mapper_context *mapper, + void *frame_buffer, + unsigned int *frame_count, + struct container_context *cntrs); +void mapper_context_post_process(struct mapper_context *mapper); +void mapper_context_destroy(struct mapper_context *mapper); + +// For internal use in 'mapper' module. + +struct mapper_ops { + int (*pre_process)(struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int cntr_count); + int (*process_frames)(struct mapper_context *mapper, + void *frame_buffer, unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count); + void (*post_process)(struct mapper_context *mapper); +}; + +struct mapper_data { + struct mapper_ops ops; + unsigned int private_size; +}; + +extern const struct mapper_data mapper_muxer_single; +extern const struct mapper_data mapper_demuxer_single; + +extern const struct mapper_data mapper_muxer_multiple; +extern const struct mapper_data mapper_demuxer_multiple; + +#endif diff --git a/axfer/misc.h b/axfer/misc.h new file mode 100644 index 0000000..5f27d28 --- /dev/null +++ b/axfer/misc.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// misc.h - a header file for miscellaneous tools. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_MISC__H_ +#define __ALSA_UTILS_AXFER_MISC__H_ + +#include + +#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0])) + +char *arg_duplicate_string(const char *str, int *err); +long arg_parse_decimal_num(const char *str, int *err); + +#endif diff --git a/axfer/subcmd-list.c b/axfer/subcmd-list.c new file mode 100644 index 0000000..187e1d7 --- /dev/null +++ b/axfer/subcmd-list.c @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// subcmd-list.c - operations for list sub command. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "subcmd.h" +#include "misc.h" + +#include +#include + +enum list_op { + LIST_OP_DEVICE = 0, + LIST_OP_PCM, + LIST_OP_HELP, +}; + +static int dump_device(snd_ctl_t *handle, const char *id, const char *name, + snd_pcm_stream_t stream ATTRIBUTE_UNUSED, snd_pcm_info_t *info) +{ + unsigned int i, count; + int err; + + printf("card %i: %s [%s], device %i: %s [%s]\n", + snd_pcm_info_get_card(info), id, name, + snd_pcm_info_get_device(info), snd_pcm_info_get_id(info), + snd_pcm_info_get_name(info)); + + count = snd_pcm_info_get_subdevices_count(info); + printf(" Subdevices: %i/%u\n", + snd_pcm_info_get_subdevices_avail(info), count); + + for (i = 0; i < count; ++i) { + snd_pcm_info_set_subdevice(info, i); + + err = snd_ctl_pcm_info(handle, info); + if (err < 0) { + printf("control digital audio playback info (%i): %s", + snd_pcm_info_get_card(info), snd_strerror(err)); + continue; + } + + printf(" Subdevice #%u: %s\n", + i, snd_pcm_info_get_subdevice_name(info)); + } + + return 0; +} + +static int dump_devices(snd_ctl_t *handle, const char *id, const char *name, + snd_pcm_stream_t direction) +{ + snd_pcm_info_t *info; + int device = -1; + int err; + + err = snd_pcm_info_malloc(&info); + if (err < 0) + return err; + + while (1) { + err = snd_ctl_pcm_next_device(handle, &device); + if (err < 0) + break; + if (device < 0) + break; + + snd_pcm_info_set_device(info, device); + snd_pcm_info_set_subdevice(info, 0); + snd_pcm_info_set_stream(info, direction); + err = snd_ctl_pcm_info(handle, info); + if (err < 0) + continue; + + err = dump_device(handle, id, name, direction, info); + if (err < 0) + break; + } + + free(info); + return err; +} + +static int list_devices(snd_pcm_stream_t direction) +{ + int card = -1; + char name[32]; + snd_ctl_t *handle; + snd_ctl_card_info_t *info; + int err; + + err = snd_ctl_card_info_malloc(&info); + if (err < 0) + return err; + + // Not found. + if (snd_card_next(&card) < 0 || card < 0) + goto end; + + printf("**** List of %s Hardware Devices ****\n", + snd_pcm_stream_name(direction)); + + while (card >= 0) { + sprintf(name, "hw:%d", card); + err = snd_ctl_open(&handle, name, 0); + if (err < 0) { + printf("control open (%i): %s", + card, snd_strerror(err)); + } else { + err = snd_ctl_card_info(handle, info); + if (err < 0) { + printf("control hardware info (%i): %s", + card, snd_strerror(err)); + } else { + err = dump_devices(handle, + snd_ctl_card_info_get_id(info), + snd_ctl_card_info_get_name(info), + direction); + } + snd_ctl_close(handle); + } + + if (err < 0) + break; + + // Go to next. + if (snd_card_next(&card) < 0) { + printf("snd_card_next"); + break; + } + } +end: + free(info); + return err; +} + +static int list_pcms(snd_pcm_stream_t direction) +{ + static const char *const filters[] = { + [SND_PCM_STREAM_CAPTURE] = "Input", + [SND_PCM_STREAM_PLAYBACK] = "Output", + }; + const char *filter; + void **hints; + void **n; + char *io; + char *name; + char *desc; + + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return -EINVAL; + + filter = filters[direction]; + + for (n = hints; *n != NULL; ++n) { + io = snd_device_name_get_hint(*n, "IOID"); + if (io != NULL && strcmp(io, filter) != 0) { + free(io); + continue; + } + + name = snd_device_name_get_hint(*n, "NAME"); + desc = snd_device_name_get_hint(*n, "DESC"); + + printf("%s\n", name); + if (desc == NULL) { + free(name); + free(desc); + continue; + } + + + printf(" "); + while (*desc) { + if (*desc == '\n') + printf("\n "); + else + putchar(*desc); + desc++; + } + putchar('\n'); + } + + snd_device_name_free_hint(hints); + + return 0; +} + +static void print_help(void) +{ + printf( +"Usage:\n" +" axfer list DIRECTION TARGET\n" +"\n" +" where:\n" +" DIRECTION = capture | playback\n" +" TARGET = device | pcm\n" + ); +} + +// Backward compatibility to aplay(1). +static bool decide_operation(int argc, char *const *argv, enum list_op *op) +{ + static const char *s_opts = "hlL"; + static const struct option l_opts[] = { + {"list-devices", 0, NULL, 'l'}, + {"list-pcms", 0, NULL, 'L'}, + {NULL, 0, NULL, 0} + }; + + optind = 0; + opterr = 0; + while (1) { + int c = getopt_long(argc, argv, s_opts, l_opts, NULL); + if (c < 0) + break; + if (c == 'l') { + *op = LIST_OP_DEVICE; + return true; + } + if (c == 'L') { + *op = LIST_OP_PCM; + return true; + } + } + + return false; +} + +static int detect_operation(int argc, char *const *argv, enum list_op *op) +{ + static const char *const ops[] = { + [LIST_OP_DEVICE] = "device", + [LIST_OP_PCM] = "pcm", + }; + int i; + + if (argc < 2) + return false; + + for (i = 0; i < (int)ARRAY_SIZE(ops); ++i) { + if (!strcmp(argv[1], ops[i])) { + *op = i; + return true; + } + } + + return false; +} + +int subcmd_list(int argc, char *const *argv, snd_pcm_stream_t direction) +{ + enum list_op op = LIST_OP_HELP; + int err = 0; + + // Renewed command system. + if (!detect_operation(argc, argv, &op) && + !decide_operation(argc, argv, &op)) + err = -EINVAL; + + if (op == LIST_OP_DEVICE) + err = list_devices(direction); + else if (op == LIST_OP_PCM) + err = list_pcms(direction); + else + print_help(); + + return err; +} diff --git a/axfer/subcmd-transfer.c b/axfer/subcmd-transfer.c new file mode 100644 index 0000000..8d63043 --- /dev/null +++ b/axfer/subcmd-transfer.c @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// subcmd-transfer.c - operations for transfer sub command. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer.h" +#include "subcmd.h" +#include "misc.h" + +#include +#include + +struct context { + struct xfer_context xfer; + struct mapper_context mapper; + struct container_context *cntrs; + unsigned int cntr_count; + + int *cntr_fds; + + // NOTE: To handling Unix signal. + bool interrupted; + int signal; +}; + +// NOTE: To handling Unix signal. +static struct context *ctx_ptr; + +static void handle_unix_signal_for_finish(int sig) +{ + unsigned int i; + + for (i = 0; i < ctx_ptr->cntr_count; ++i) + ctx_ptr->cntrs[i].interrupted = true; + + ctx_ptr->signal = sig; + ctx_ptr->interrupted = true; +} + +static void handle_unix_signal_for_suspend(int sig ATTRIBUTE_UNUSED) +{ + sigset_t curr, prev; + struct sigaction sa = {0}; + + // 1. suspend substream. + xfer_context_pause(&ctx_ptr->xfer, true); + + // 2. Prepare for default handler(SIG_DFL) of SIGTSTP to stop this + // process. + if (sigaction(SIGTSTP, NULL, &sa) < 0) { + fprintf(stderr, "sigaction(2)\n"); + exit(EXIT_FAILURE); + } + if (sa.sa_handler == SIG_ERR) + exit(EXIT_FAILURE); + if (sa.sa_handler == handle_unix_signal_for_suspend) + sa.sa_handler = SIG_DFL; + if (sigaction(SIGTSTP, &sa, NULL) < 0) { + fprintf(stderr, "sigaction(2)\n"); + exit(EXIT_FAILURE); + } + + // Queue SIGTSTP. + raise(SIGTSTP); + + // Release the queued signal from being blocked. This causes an + // additional interrupt for the default handler. + sigemptyset(&curr); + sigaddset(&curr, SIGTSTP); + if (sigprocmask(SIG_UNBLOCK, &curr, &prev) < 0) { + fprintf(stderr, "sigprocmask(2)\n"); + exit(EXIT_FAILURE); + } + + // 3. SIGCONT is cought and rescheduled. Recover blocking status of + // UNIX signals. + if (sigprocmask(SIG_SETMASK, &prev, NULL) < 0) { + fprintf(stderr, "sigprocmask(2)\n"); + exit(EXIT_FAILURE); + } + + // Reconfigure this handler for SIGTSTP, instead of default one. + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = handle_unix_signal_for_suspend; + if (sigaction(SIGTSTP, &sa, NULL) < 0) { + fprintf(stderr, "sigaction(2)\n"); + exit(EXIT_FAILURE); + } + + // 4. Continue the PCM substream. + xfer_context_pause(&ctx_ptr->xfer, false); +} + +static int prepare_signal_handler(struct context *ctx) +{ + struct sigaction sa = {0}; + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = handle_unix_signal_for_finish; + + if (sigaction(SIGINT, &sa, NULL) < 0) + return -errno; + if (sigaction(SIGTERM, &sa, NULL) < 0) + return -errno; + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = handle_unix_signal_for_suspend; + if (sigaction(SIGTSTP, &sa, NULL) < 0) + return -errno; + + ctx_ptr = ctx; + + return 0; +} + +static int context_init(struct context *ctx, snd_pcm_stream_t direction, + int argc, char *const *argv) +{ + const char *xfer_type_literal; + enum xfer_type xfer_type; + int i; + + // Decide transfer backend before option parser runs. + xfer_type_literal = NULL; + for (i = 0; i < argc; ++i) { + if (strstr(argv[i], "--xfer-type") != argv[i]) + continue; + xfer_type_literal = argv[i] + 12; + } + if (xfer_type_literal == NULL) { + xfer_type = XFER_TYPE_LIBASOUND; + } else { + xfer_type = xfer_type_from_label(xfer_type_literal); + if (xfer_type == XFER_TYPE_UNSUPPORTED) { + fprintf(stderr, "The '%s' xfer type is not supported\n", + xfer_type_literal); + return -EINVAL; + } + } + + // Initialize transfer. + return xfer_context_init(&ctx->xfer, xfer_type, direction, argc, argv); +} + +static int allocate_containers(struct context *ctx, unsigned int count) +{ + ctx->cntrs = calloc(count, sizeof(*ctx->cntrs)); + if (ctx->cntrs == NULL) + return -ENOMEM; + ctx->cntr_count = count; + + ctx->cntr_fds = calloc(count, sizeof(*ctx->cntr_fds)); + if (ctx->cntr_fds == NULL) + return -ENOMEM; + + return 0; +} + +static int capture_pre_process(struct context *ctx, snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer, + uint64_t *total_frame_count) +{ + snd_pcm_format_t sample_format = SND_PCM_FORMAT_UNKNOWN; + unsigned int samples_per_frame = 0; + unsigned int frames_per_second = 0; + unsigned int channels; + unsigned int i; + int err; + + err = xfer_context_pre_process(&ctx->xfer, &sample_format, + &samples_per_frame, &frames_per_second, + access, frames_per_buffer); + if (err < 0) + return err; + + // Prepare for containers. + err = allocate_containers(ctx, ctx->xfer.path_count); + if (err < 0) + return err; + + if (ctx->cntr_count > 1) + channels = 1; + else + channels = samples_per_frame; + + *total_frame_count = 0; + for (i = 0; i < ctx->cntr_count; ++i) { + const char *path = ctx->xfer.paths[i]; + int fd; + uint64_t frame_count; + + if (!strcmp(path, "-")) { + fd = fileno(stdout); + } else { + fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return -errno; + } + ctx->cntr_fds[i] = fd; + + err = container_builder_init(ctx->cntrs + i, ctx->cntr_fds[i], + ctx->xfer.cntr_format, + ctx->xfer.verbose > 1); + if (err < 0) + return err; + + err = container_context_pre_process(ctx->cntrs + i, + &sample_format, &channels, + &frames_per_second, + &frame_count); + if (err < 0) + return err; + + if (*total_frame_count == 0) + *total_frame_count = frame_count; + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } + + return 0; +} + +static int playback_pre_process(struct context *ctx, snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer, + uint64_t *total_frame_count) +{ + snd_pcm_format_t sample_format = SND_PCM_FORMAT_UNKNOWN; + unsigned int samples_per_frame = 0; + unsigned int frames_per_second = 0; + unsigned int i; + int err; + + // Prepare for containers. + err = allocate_containers(ctx, ctx->xfer.path_count); + if (err < 0) + return err; + + for (i = 0; i < ctx->cntr_count; ++i) { + const char *path = ctx->xfer.paths[i]; + int fd; + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + uint64_t frame_count; + + if (!strcmp(path, "-")) { + fd = fileno(stdin); + } else { + fd = open(path, O_RDONLY); + if (fd < 0) + return -errno; + } + ctx->cntr_fds[i] = fd; + + err = container_parser_init(ctx->cntrs + i, ctx->cntr_fds[i], + ctx->xfer.verbose > 1); + if (err < 0) + return err; + + if (i == 0) { + // For a raw container. + format = ctx->xfer.sample_format; + channels = ctx->xfer.samples_per_frame; + rate = ctx->xfer.frames_per_second; + } else { + format = sample_format; + channels = samples_per_frame; + rate = frames_per_second; + } + + err = container_context_pre_process(ctx->cntrs + i, &format, + &channels, &rate, + &frame_count); + if (err < 0) + return err; + + if (format == SND_PCM_FORMAT_UNKNOWN || channels == 0 || + rate == 0) { + fprintf(stderr, + "Sample format, channels and rate should be " + "indicated for given files.\n"); + return -EINVAL; + } + + if (i == 0) { + sample_format = format; + samples_per_frame = channels; + frames_per_second = rate; + *total_frame_count = frame_count; + } else { + if (format != sample_format) { + fprintf(stderr, + "When using several files, they " + "should include the same sample " + "format.\n"); + return -EINVAL; + } + + // No need to check channels to handle multiple + // containers. + if (rate != frames_per_second) { + fprintf(stderr, + "When using several files, they " + "should include samples at the same " + "sampling rate.\n"); + return -EINVAL; + } + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } + } + + if (ctx->cntr_count > 1) + samples_per_frame = ctx->cntr_count; + + // Configure hardware with these parameters. + return xfer_context_pre_process(&ctx->xfer, &sample_format, + &samples_per_frame, &frames_per_second, + access, frames_per_buffer); +} + +static int context_pre_process(struct context *ctx, snd_pcm_stream_t direction, + uint64_t *total_frame_count) +{ + snd_pcm_access_t access; + snd_pcm_uframes_t frames_per_buffer = 0; + unsigned int bytes_per_sample = 0; + enum mapper_type mapper_type; + int err; + + if (direction == SND_PCM_STREAM_CAPTURE) { + mapper_type = MAPPER_TYPE_DEMUXER; + err = capture_pre_process(ctx, &access, &frames_per_buffer, + total_frame_count); + } else { + mapper_type = MAPPER_TYPE_MUXER; + err = playback_pre_process(ctx, &access, &frames_per_buffer, + total_frame_count); + } + if (err < 0) + return err; + + // Prepare for mapper. + err = mapper_context_init(&ctx->mapper, mapper_type, ctx->cntr_count, + ctx->xfer.verbose > 1); + if (err < 0) + return err; + + bytes_per_sample = + snd_pcm_format_physical_width(ctx->xfer.sample_format) / 8; + if (bytes_per_sample <= 0) + return -ENXIO; + err = mapper_context_pre_process(&ctx->mapper, access, bytes_per_sample, + ctx->xfer.samples_per_frame, + frames_per_buffer, ctx->cntrs); + if (err < 0) + return err; + + xfer_options_calculate_duration(&ctx->xfer, total_frame_count); + + return 0; +} + +static int context_process_frames(struct context *ctx, + snd_pcm_stream_t direction, + uint64_t expected_frame_count, + uint64_t *actual_frame_count) +{ + bool verbose = ctx->xfer.verbose > 2; + unsigned int frame_count; + unsigned int i; + int err = 0; + + if (!ctx->xfer.quiet) { + fprintf(stderr, + "%s: Format '%s', Rate %u Hz, Channels ", + snd_pcm_stream_name(direction), + snd_pcm_format_description(ctx->xfer.sample_format), + ctx->xfer.frames_per_second); + if (ctx->xfer.samples_per_frame == 1) + fprintf(stderr, "'monaural'"); + else if (ctx->xfer.samples_per_frame == 2) + fprintf(stderr, "'Stereo'"); + else + fprintf(stderr, "%u", ctx->xfer.samples_per_frame); + fprintf(stderr, "\n"); + } + + *actual_frame_count = 0; + while (!ctx->interrupted) { + struct container_context *cntr; + + // Tell remains to expected frame count. + frame_count = expected_frame_count - *actual_frame_count; + err = xfer_context_process_frames(&ctx->xfer, &ctx->mapper, + ctx->cntrs, &frame_count); + if (err < 0) { + if (err == -EAGAIN || err == -EINTR) + continue; + break; + } + if (verbose) { + fprintf(stderr, + " handled: %u\n", frame_count); + } + for (i = 0; i < ctx->cntr_count; ++i) { + cntr = &ctx->cntrs[i]; + if (cntr->eof) + break; + } + if (i < ctx->cntr_count) + break; + + *actual_frame_count += frame_count; + if (*actual_frame_count >= expected_frame_count) + break; + } + + if (!ctx->xfer.quiet) { + fprintf(stderr, + "%s: Expected %" PRIu64 "frames, " + "Actual %" PRIu64 "frames\n", + snd_pcm_stream_name(direction), expected_frame_count, + *actual_frame_count); + if (ctx->interrupted) { + fprintf(stderr, "Aborted by signal: %s\n", + strsignal(ctx->signal)); + return 0; + } + } + + return err; +} + +static void context_post_process(struct context *ctx, + uint64_t accumulated_frame_count ATTRIBUTE_UNUSED) +{ + uint64_t total_frame_count; + unsigned int i; + + xfer_context_post_process(&ctx->xfer); + + if (ctx->cntrs) { + for (i = 0; i < ctx->cntr_count; ++i) { + container_context_post_process(ctx->cntrs + i, + &total_frame_count); + container_context_destroy(ctx->cntrs + i); + } + free(ctx->cntrs); + } + + if (ctx->cntr_fds) { + for (i = 0; i < ctx->cntr_count; ++i) + close(ctx->cntr_fds[i]); + free(ctx->cntr_fds); + } + + mapper_context_post_process(&ctx->mapper); + mapper_context_destroy(&ctx->mapper); +} + +static void context_destroy(struct context *ctx) +{ + xfer_context_destroy(&ctx->xfer); +} + +int subcmd_transfer(int argc, char *const *argv, snd_pcm_stream_t direction) +{ + static struct context ctx = {0}; + uint64_t expected_frame_count = 0; + uint64_t actual_frame_count = 0; + int err = 0; + + err = prepare_signal_handler(&ctx); + if (err < 0) + return err; + + err = context_init(&ctx, direction, argc, argv); + if (err < 0) + goto end; + if (ctx.xfer.help || ctx.xfer.dump_hw_params) + goto end; + + err = context_pre_process(&ctx, direction, &expected_frame_count); + if (err < 0) + goto end; + + err = context_process_frames(&ctx, direction, expected_frame_count, + &actual_frame_count); +end: + context_post_process(&ctx, actual_frame_count); + + context_destroy(&ctx); + + return err; +} diff --git a/axfer/subcmd.h b/axfer/subcmd.h new file mode 100644 index 0000000..5ffe1e4 --- /dev/null +++ b/axfer/subcmd.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// subcmd.h - a header for each sub-commands. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_SUBCMD__H_ +#define __ALSA_UTILS_AXFER_SUBCMD__H_ + +#include + +int subcmd_list(int argc, char *const *argv, snd_pcm_stream_t direction); + +int subcmd_transfer(int argc, char *const *argv, snd_pcm_stream_t direction); + +#endif diff --git a/axfer/test/Makefile.am b/axfer/test/Makefile.am new file mode 100644 index 0000000..6521134 --- /dev/null +++ b/axfer/test/Makefile.am @@ -0,0 +1,33 @@ +TESTS = \ + container-test \ + mapper-test + +check_PROGRAMS = \ + container-test \ + mapper-test + +container_test_SOURCES = \ + ../container.h \ + ../container.c \ + ../container-riff-wave.c \ + ../container-au.c \ + ../container-voc.c \ + ../container-raw.c \ + generator.c \ + generator.h \ + container-test.c + +mapper_test_SOURCES = \ + ../container.h \ + ../container.c \ + ../container-riff-wave.c \ + ../container-au.c \ + ../container-voc.c \ + ../container-raw.c \ + ../mapper.h \ + ../mapper.c \ + ../mapper-single.c \ + ../mapper-multiple.c \ + generator.c \ + generator.h \ + mapper-test.c diff --git a/axfer/test/container-test.c b/axfer/test/container-test.c new file mode 100644 index 0000000..45b6c6a --- /dev/null +++ b/axfer/test/container-test.c @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-io.c - a unit test for parser/builder of supported containers. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include +#ifdef HAVE_MEMFD_CREATE +#define _GNU_SOURCE +#endif + +#include "../container.h" +#include "../misc.h" + +#include "generator.h" + +#ifdef HAVE_MEMFD_CREATE +#include +#endif + +#include +#include +#include + +#include + +struct container_trial { + enum container_format format; + + struct container_context cntr; + bool verbose; +}; + +static void test_builder(struct container_context *cntr, int fd, + enum container_format format, + snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + void *frame_buffer, unsigned int frame_count, + bool verbose) +{ + snd_pcm_format_t sample; + unsigned int channels; + unsigned int rate; + uint64_t max_frame_count; + unsigned int handled_frame_count; + uint64_t total_frame_count; + int err; + + err = container_builder_init(cntr, fd, format, verbose); + assert(err == 0); + + sample = sample_format; + channels = samples_per_frame; + rate = frames_per_second; + max_frame_count = 0; + err = container_context_pre_process(cntr, &sample, &channels, &rate, + &max_frame_count); + assert(err == 0); + assert(sample == sample_format); + assert(channels == samples_per_frame); + assert(rate == frames_per_second); + assert(max_frame_count > 0); + + handled_frame_count = frame_count; + err = container_context_process_frames(cntr, frame_buffer, + &handled_frame_count); + assert(err == 0); + assert(handled_frame_count > 0); + assert(handled_frame_count <= frame_count); + + total_frame_count = 0; + err = container_context_post_process(cntr, &total_frame_count); + assert(err == 0); + assert(total_frame_count == frame_count); + + container_context_destroy(cntr); +} + +static void test_parser(struct container_context *cntr, int fd, + enum container_format format, + snd_pcm_access_t access, snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + void *frame_buffer, unsigned int frame_count, + bool verbose) +{ + snd_pcm_format_t sample; + unsigned int channels; + unsigned int rate; + uint64_t total_frame_count; + unsigned int handled_frame_count; + int err; + + err = container_parser_init(cntr, fd, verbose); + assert(err == 0); + + sample = sample_format; + channels = samples_per_frame; + rate = frames_per_second; + total_frame_count = 0; + err = container_context_pre_process(cntr, &sample, &channels, &rate, + &total_frame_count); + assert(err == 0); + assert(sample == sample_format); + assert(channels == samples_per_frame); + assert(rate == frames_per_second); + assert(total_frame_count == frame_count); + + handled_frame_count = total_frame_count; + err = container_context_process_frames(cntr, frame_buffer, + &handled_frame_count); + assert(err == 0); + assert(handled_frame_count == frame_count); + + total_frame_count = 0; + err = container_context_post_process(cntr, &total_frame_count); + assert(err == 0); + assert(total_frame_count == handled_frame_count); + + container_context_destroy(cntr); +} + +static int callback(struct test_generator *gen, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, void *frame_buffer, + unsigned int frame_count) +{ + static const unsigned int entries[] = { + [0] = 44100, + [1] = 48000, + [2] = 88200, + [3] = 96000, + [4] = 176400, + [5] = 192000, + }; + struct container_trial *trial = gen->private_data; + unsigned int frames_per_second; + const char *const name = "hoge"; + unsigned int size; + void *buf; + int i; + int err = 0; + + size = frame_count * samples_per_frame * + snd_pcm_format_physical_width(sample_format) / 8; + buf = malloc(size); + if (buf == NULL) + return -ENOMEM; + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + int fd; + off_t pos; + + frames_per_second = entries[i]; + +#ifdef HAVE_MEMFD_CREATE + fd = memfd_create(name, 0); +#else + fd = open(name, O_RDWR | O_CREAT | O_TRUNC, 0644); +#endif + if (fd < 0) { + err = -errno; + break; + } + + test_builder(&trial->cntr, fd, trial->format, access, + sample_format, samples_per_frame, + frames_per_second, frame_buffer, frame_count, + trial->verbose); + + pos = lseek(fd, 0, SEEK_SET); + if (pos < 0) { + err = -errno; + break; + } + + test_parser(&trial->cntr, fd, trial->format, access, + sample_format, samples_per_frame, frames_per_second, + buf, frame_count, trial->verbose); + + err = memcmp(buf, frame_buffer, size); + assert(err == 0); + + close(fd); + } + + free(buf); + + return err; +} + +int main(int argc, const char *argv[]) +{ + static const uint64_t sample_format_masks[] = { + [CONTAINER_FORMAT_RIFF_WAVE] = + (1ull << SND_PCM_FORMAT_U8) | + (1ull << SND_PCM_FORMAT_S16_LE) | + (1ull << SND_PCM_FORMAT_S16_BE) | + (1ull << SND_PCM_FORMAT_S24_LE) | + (1ull << SND_PCM_FORMAT_S24_BE) | + (1ull << SND_PCM_FORMAT_S32_LE) | + (1ull << SND_PCM_FORMAT_S32_BE) | + (1ull << SND_PCM_FORMAT_FLOAT_LE) | + (1ull << SND_PCM_FORMAT_FLOAT_BE) | + (1ull << SND_PCM_FORMAT_FLOAT64_LE) | + (1ull << SND_PCM_FORMAT_FLOAT64_BE) | + (1ull << SND_PCM_FORMAT_MU_LAW) | + (1ull << SND_PCM_FORMAT_A_LAW) | + (1ull << SND_PCM_FORMAT_S24_3LE) | + (1ull << SND_PCM_FORMAT_S24_3BE) | + (1ull << SND_PCM_FORMAT_S20_3LE) | + (1ull << SND_PCM_FORMAT_S20_3BE) | + (1ull << SND_PCM_FORMAT_S18_3LE) | + (1ull << SND_PCM_FORMAT_S18_3BE), + [CONTAINER_FORMAT_AU] = + (1ull << SND_PCM_FORMAT_S8) | + (1ull << SND_PCM_FORMAT_S16_BE) | + (1ull << SND_PCM_FORMAT_S32_BE) | + (1ull << SND_PCM_FORMAT_FLOAT_BE) | + (1ull << SND_PCM_FORMAT_FLOAT64_BE) | + (1ull << SND_PCM_FORMAT_MU_LAW) | + (1ull << SND_PCM_FORMAT_A_LAW), + [CONTAINER_FORMAT_VOC] = + (1ull << SND_PCM_FORMAT_U8) | + (1ull << SND_PCM_FORMAT_S16_LE) | + (1ull << SND_PCM_FORMAT_MU_LAW) | + (1ull << SND_PCM_FORMAT_A_LAW), + [CONTAINER_FORMAT_RAW] = + (1ull << SND_PCM_FORMAT_S8) | + (1ull << SND_PCM_FORMAT_U8) | + (1ull << SND_PCM_FORMAT_S16_LE) | + (1ull << SND_PCM_FORMAT_S16_BE) | + (1ull << SND_PCM_FORMAT_U16_LE) | + (1ull << SND_PCM_FORMAT_U16_BE) | + (1ull << SND_PCM_FORMAT_S24_LE) | + (1ull << SND_PCM_FORMAT_S24_BE) | + (1ull << SND_PCM_FORMAT_U24_LE) | + (1ull << SND_PCM_FORMAT_U24_BE) | + (1ull << SND_PCM_FORMAT_S32_LE) | + (1ull << SND_PCM_FORMAT_S32_BE) | + (1ull << SND_PCM_FORMAT_U32_LE) | + (1ull << SND_PCM_FORMAT_U32_BE) | + (1ull << SND_PCM_FORMAT_FLOAT_LE) | + (1ull << SND_PCM_FORMAT_FLOAT_BE) | + (1ull << SND_PCM_FORMAT_FLOAT64_LE) | + (1ull << SND_PCM_FORMAT_FLOAT64_BE) | + (1ull << SND_PCM_FORMAT_IEC958_SUBFRAME_LE) | + (1ull << SND_PCM_FORMAT_IEC958_SUBFRAME_BE) | + (1ull << SND_PCM_FORMAT_MU_LAW) | + (1ull << SND_PCM_FORMAT_A_LAW) | + (1ull << SND_PCM_FORMAT_S24_3LE) | + (1ull << SND_PCM_FORMAT_S24_3BE) | + (1ull << SND_PCM_FORMAT_U24_3LE) | + (1ull << SND_PCM_FORMAT_U24_3BE) | + (1ull << SND_PCM_FORMAT_S20_3LE) | + (1ull << SND_PCM_FORMAT_S20_3BE) | + (1ull << SND_PCM_FORMAT_U20_3LE) | + (1ull << SND_PCM_FORMAT_U20_3BE) | + (1ull << SND_PCM_FORMAT_S18_3LE) | + (1ull << SND_PCM_FORMAT_S18_3BE) | + (1ull << SND_PCM_FORMAT_U18_3LE) | + (1ull << SND_PCM_FORMAT_U18_3BE) | + (1ull << SND_PCM_FORMAT_DSD_U8) | + (1ull << SND_PCM_FORMAT_DSD_U16_LE) | + (1ull << SND_PCM_FORMAT_DSD_U32_LE) | + (1ull << SND_PCM_FORMAT_DSD_U16_BE) | + (1ull << SND_PCM_FORMAT_DSD_U32_BE), + }; + static const uint64_t access_mask = + (1ull << SND_PCM_ACCESS_MMAP_INTERLEAVED) | + (1ull << SND_PCM_ACCESS_RW_INTERLEAVED); + struct test_generator gen = {0}; + struct container_trial *trial; + int i; + int begin; + int end; + bool verbose; + int err; + + if (argc > 1) { + char *term; + begin = strtol(argv[1], &term, 10); + if (errno || *term != '\0') + return EXIT_FAILURE; + if (begin < CONTAINER_FORMAT_RIFF_WAVE && + begin > CONTAINER_FORMAT_RAW) + return -EXIT_FAILURE; + end = begin + 1; + verbose = true; + } else { + begin = CONTAINER_FORMAT_RIFF_WAVE; + end = CONTAINER_FORMAT_RAW + 1; + verbose = false; + } + + for (i = begin; i < end; ++i) { + err = generator_context_init(&gen, access_mask, + sample_format_masks[i], + 1, 32, 23, 3000, 512, + sizeof(struct container_trial)); + if (err >= 0) { + trial = gen.private_data; + trial->format = i; + trial->verbose = verbose; + err = generator_context_run(&gen, callback); + } + + generator_context_destroy(&gen); + + if (err < 0) + break; + } + + if (err < 0) { + printf("%s\n", strerror(-err)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/axfer/test/generator.c b/axfer/test/generator.c new file mode 100644 index 0000000..bde8c5f --- /dev/null +++ b/axfer/test/generator.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// allocator.h - a header of a generator for test with buffers of PCM frames. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "generator.h" + +#include + +#include +#include +#include + +#include + +int generator_context_init(struct test_generator *gen, + uint64_t access_mask, uint64_t sample_format_mask, + unsigned int min_samples_per_frame, + unsigned int max_samples_per_frame, + unsigned int min_frame_count, + unsigned int max_frame_count, + unsigned int step_frame_count, + unsigned int private_size) +{ + gen->fd = open("/dev/urandom", O_RDONLY); + if (gen->fd < 0) + return -errno; + + gen->private_data = malloc(private_size); + if (gen->private_data == NULL) + return -ENOMEM; + memset(gen->private_data, 0, private_size); + + gen->access_mask = access_mask; + gen->sample_format_mask = sample_format_mask; + gen->min_samples_per_frame = min_samples_per_frame; + gen->max_samples_per_frame = max_samples_per_frame; + gen->min_frame_count = min_frame_count; + gen->max_frame_count = max_frame_count; + gen->step_frame_count = step_frame_count; + + return 0; +} + +static void *allocate_buf(snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frame_count) +{ + unsigned int bytes_per_sample; + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + + return calloc(samples_per_frame * frame_count, bytes_per_sample); +} + +static void *allocate_vector(snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frame_count) +{ + unsigned int bytes_per_sample; + char **bufs; + int i; + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + + bufs = calloc(samples_per_frame, sizeof(char *)); + if (bufs == NULL) + return NULL; + + for (i = 0; i < samples_per_frame; ++i) { + bufs[i] = calloc(frame_count, bytes_per_sample); + if (bufs[i] == NULL) { + for (; i >= 0; --i) + free(bufs[i]); + free(bufs); + return NULL; + } + } + + return bufs; +} + +static int fill_buf(int fd, void *frame_buffer, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, unsigned int frame_count) +{ + unsigned int size; + int len; + + size = snd_pcm_format_physical_width(sample_format) / 8 * + samples_per_frame * frame_count; + while (size > 0) { + len = read(fd, frame_buffer, size); + if (len < 0) + return len; + size -= len; + } + + return 0; +} + +static int fill_vector(int fd, void *frame_buffer, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, unsigned int frame_count) +{ + char **bufs = frame_buffer; + unsigned int size; + int len; + int i; + + for (i = 0; i < samples_per_frame; ++i) { + size = frame_count * + snd_pcm_format_physical_width(sample_format) / 8; + + while (size > 0) { + len = read(fd, bufs[i], size); + if (len < 0) + return len; + size -= len; + } + } + + return 0; +} + +static void deallocate_buf(void *frame_buffer, unsigned int samples_per_frame) +{ + free(frame_buffer); +} + +static void deallocate_vector(void *frame_buffer, + unsigned int samples_per_frame) +{ + char **bufs = frame_buffer; + int i; + + for (i = 0; i < samples_per_frame; ++i) + free(bufs[i]); + + free(bufs); +} + +static int test_frame_count(struct test_generator *gen, + snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame) +{ + void *(*allocator)(snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frame_count); + int (*fill)(int fd, void *frame_buffer, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, unsigned int frame_count); + void (*deallocator)(void *frame_buffer, unsigned int samples_per_frame); + void *frame_buffer; + int i; + int err = 0; + + if (access != SND_PCM_ACCESS_RW_NONINTERLEAVED) { + allocator = allocate_buf; + fill = fill_buf; + deallocator = deallocate_buf; + } else { + allocator = allocate_vector; + fill = fill_vector; + deallocator = deallocate_vector; + } + + frame_buffer = allocator(access, sample_format, samples_per_frame, + gen->max_frame_count); + if (frame_buffer == NULL) + return -ENOMEM; + + err = fill(gen->fd, frame_buffer, access, sample_format, + samples_per_frame, gen->max_frame_count); + if (err < 0) + goto end; + + + for (i = gen->min_frame_count; + i <= gen->max_frame_count; i += gen->step_frame_count) { + err = gen->cb(gen, access ,sample_format, samples_per_frame, + frame_buffer, i); + if (err < 0) + break; + } +end: + deallocator(frame_buffer, samples_per_frame); + + return err; +} + +static int test_samples_per_frame(struct test_generator *gen, + snd_pcm_access_t access, + snd_pcm_format_t sample_format) +{ + int i; + int err = 0; + + for (i = gen->min_samples_per_frame; + i <= gen->max_samples_per_frame; ++i) { + err = test_frame_count(gen, access, sample_format, i); + if (err < 0) + break; + } + + return err; +} + +static int test_sample_format(struct test_generator *gen, + snd_pcm_access_t access) +{ + int i; + int err = 0; + + for (i = 0; i <= SND_PCM_FORMAT_LAST; ++i) { + if (!((1ull << i) & gen->sample_format_mask)) + continue; + + err = test_samples_per_frame(gen, access, i); + if (err < 0) + break; + } + + return err; +} + +static int test_access(struct test_generator *gen) +{ + int i; + int err = 0; + + for (i = 0; i <= SND_PCM_ACCESS_LAST; ++i) { + if (!((1ull << i) & gen->access_mask)) + continue; + + err = test_sample_format(gen, i); + if (err < 0) + break; + } + return err; +} + +int generator_context_run(struct test_generator *gen, generator_cb_t cb) +{ + gen->cb = cb; + return test_access(gen); +} + +void generator_context_destroy(struct test_generator *gen) +{ + free(gen->private_data); + close(gen->fd); +} diff --git a/axfer/test/generator.h b/axfer/test/generator.h new file mode 100644 index 0000000..c35ed98 --- /dev/null +++ b/axfer/test/generator.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// generator.c - a generator for test with buffers of PCM frames. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_TEST_GENERATOR__H_ +#define __ALSA_UTILS_AXFER_TEST_GENERATOR__H_ + +#include +#include + +struct test_generator; +typedef int (*generator_cb_t)(struct test_generator *gen, + snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + void *frame_buffer, unsigned int frame_count); + +struct test_generator { + int fd; + uint64_t access_mask; + uint64_t sample_format_mask; + unsigned int min_samples_per_frame; + unsigned int max_samples_per_frame; + unsigned int min_frame_count; + unsigned int max_frame_count; + unsigned int step_frame_count; + + generator_cb_t cb; + void *private_data; +}; + +int generator_context_init(struct test_generator *gen, + uint64_t access_mask, uint64_t sample_format_mask, + unsigned int min_samples_per_frame, + unsigned int max_samples_per_frame, + unsigned int min_frame_count, + unsigned int max_frame_count, + unsigned int step_frame_count, + unsigned int private_size); +int generator_context_run(struct test_generator *gen, generator_cb_t cb); +void generator_context_destroy(struct test_generator *gen); + +#endif diff --git a/axfer/test/mapper-test.c b/axfer/test/mapper-test.c new file mode 100644 index 0000000..6a389e9 --- /dev/null +++ b/axfer/test/mapper-test.c @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper-io.c - a unit test for muxer/demuxer for PCM frames on buffer. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include +#ifdef HAVE_MEMFD_CREATE +#define _GNU_SOURCE +#endif + +#include "../mapper.h" +#include "../misc.h" + +#include "generator.h" + +#ifdef HAVE_MEMFD_CREATE +#include +#endif + +#include +#include +#include + +#include + +struct mapper_trial { + enum container_format cntr_format; + struct container_context *cntrs; + + char **paths; + + struct mapper_context mapper; + bool verbose; +}; + +static void test_demuxer(struct mapper_context *mapper, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + struct container_context *cntrs, + unsigned int cntr_count, bool verbose) +{ + unsigned int total_frame_count; + int err; + + err = mapper_context_init(mapper, MAPPER_TYPE_DEMUXER, cntr_count, + verbose); + assert(err == 0); + + err = mapper_context_pre_process(mapper, access, bytes_per_sample, + samples_per_frame, frames_per_buffer, + cntrs); + assert(err == 0); + + total_frame_count = frame_count; + err = mapper_context_process_frames(mapper, frame_buffer, + &total_frame_count, cntrs); + assert(err == 0); + assert(total_frame_count == frame_count); + + mapper_context_post_process(mapper); + mapper_context_destroy(mapper); +} + +static int test_demux(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + int *cntr_fds, unsigned int cntr_count) +{ + struct container_context *cntrs = trial->cntrs; + enum container_format cntr_format = trial->cntr_format; + unsigned int bytes_per_sample; + uint64_t total_frame_count; + int i; + int err = 0; + + for (i = 0; i < cntr_count; ++i) { + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + + err = container_builder_init(cntrs + i, cntr_fds[i], cntr_format, 0); + if (err < 0) + goto end; + + format = sample_format; + rate = frames_per_second; + total_frame_count = frame_count; + if (cntr_count > 1) + channels = 1; + else + channels = samples_per_frame; + err = container_context_pre_process(cntrs + i, &format, + &channels, &rate, + &total_frame_count); + if (err < 0) + goto end; + assert(format == sample_format); + assert(rate == frames_per_second); + assert(total_frame_count >= 0); + if (cntr_count > 1) + assert(channels == 1); + else + assert(channels == samples_per_frame); + } + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + test_demuxer(&trial->mapper, access, bytes_per_sample, + samples_per_frame, frames_per_buffer, frame_buffer, + frame_count, cntrs, cntr_count, trial->verbose); + + for (i = 0; i < cntr_count; ++i) { + container_context_post_process(cntrs + i, &total_frame_count); + assert(total_frame_count == frame_count); + } +end: + for (i = 0; i < cntr_count; ++i) + container_context_destroy(cntrs + i); + + return err; +} + +static void test_muxer(struct mapper_context *mapper, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + struct container_context *cntrs, + unsigned int cntr_count, bool verbose) +{ + unsigned int total_frame_count; + int err; + + err = mapper_context_init(mapper, MAPPER_TYPE_MUXER, cntr_count, + verbose); + assert(err == 0); + + err = mapper_context_pre_process(mapper, access, bytes_per_sample, + samples_per_frame, frames_per_buffer, + cntrs); + assert(err == 0); + + total_frame_count = frame_count; + err = mapper_context_process_frames(mapper, frame_buffer, + &total_frame_count, cntrs); + assert(err == 0); + assert(total_frame_count == frame_count); + + mapper_context_post_process(mapper); + mapper_context_destroy(mapper); +} + +static int test_mux(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + int *cntr_fds, unsigned int cntr_count) +{ + struct container_context *cntrs = trial->cntrs; + unsigned int bytes_per_sample; + uint64_t total_frame_count; + int i; + int err = 0; + + for (i = 0; i < cntr_count; ++i) { + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + + err = container_parser_init(cntrs + i, cntr_fds[i], 0); + if (err < 0) + goto end; + + format = sample_format; + rate = frames_per_second; + if (cntr_count > 1) + channels = 1; + else + channels = samples_per_frame; + err = container_context_pre_process(cntrs + i, &format, + &channels, &rate, + &total_frame_count); + if (err < 0) + goto end; + + assert(format == sample_format); + assert(rate == frames_per_second); + assert(total_frame_count == frame_count); + if (cntr_count > 1) + assert(channels == 1); + else + assert(channels == samples_per_frame); + } + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + test_muxer(&trial->mapper, access, bytes_per_sample, samples_per_frame, + frames_per_buffer, frame_buffer, frame_count, cntrs, + cntr_count, trial->verbose); + + for (i = 0; i < cntr_count; ++i) { + container_context_post_process(cntrs + i, &total_frame_count); + assert(total_frame_count == frame_count); + } +end: + for (i = 0; i < cntr_count; ++i) + container_context_destroy(cntrs + i); + + return err; +} + +static int test_mapper(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + void *check_buffer, unsigned int frame_count, + unsigned int cntr_count) +{ + int *cntr_fds; + unsigned int frames_per_buffer; + int i; + int err; + + // Use a buffer aligned by typical size of page frame. + frames_per_buffer = ((frame_count + 4096) / 4096) * 4096; + + cntr_fds = calloc(cntr_count, sizeof(*cntr_fds)); + if (cntr_fds == NULL) + return -ENOMEM; + + for (i = 0; i < cntr_count; ++i) { + const char *path = trial->paths[i]; + +#ifdef HAVE_MEMFD_CREATE + cntr_fds[i] = memfd_create(path, 0); +#else + cntr_fds[i] = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); +#endif + if (cntr_fds[i] < 0) { + err = -errno; + goto end; + } + } + + err = test_demux(trial, access, sample_format, samples_per_frame, + frames_per_second, frames_per_buffer, frame_buffer, + frame_count, cntr_fds, cntr_count); + if (err < 0) + goto end; + + for (i = 0; i < cntr_count; ++i) { + off_t pos = lseek(cntr_fds[i], 0, SEEK_SET); + if (pos != 0) { + err = -EIO; + goto end; + } + } + + err = test_mux(trial, access, sample_format, samples_per_frame, + frames_per_second, frames_per_buffer, check_buffer, + frame_count, cntr_fds, cntr_count); +end: + for (i = 0; i < cntr_count; ++i) + close(cntr_fds[i]); + + free(cntr_fds); + + return err; +} + +static int test_i_buf(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count) +{ + unsigned int size; + char *buf; + int err; + + size = frame_count * samples_per_frame * + snd_pcm_format_physical_width(sample_format) / 8; + buf = malloc(size); + if (buf == 0) + return -ENOMEM; + memset(buf, 0, size); + + // Test multiple target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, buf, + frame_count, cntr_count); + if (err < 0) + goto end; + err = memcmp(frame_buffer, buf, size); + assert(err == 0); + + // Test single target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, buf, + frame_count, 1); + if (err < 0) + goto end; + err = memcmp(frame_buffer, buf, size); + assert(err == 0); +end: + free(buf); + + return err; +} + +static int test_vector(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count) +{ + unsigned int size; + char **bufs; + int i; + int err; + + bufs = calloc(cntr_count, sizeof(*bufs)); + if (bufs == NULL) + return -ENOMEM; + + size = frame_count * snd_pcm_format_physical_width(sample_format) / 8; + + for (i = 0; i < cntr_count; ++i) { + bufs[i] = malloc(size); + if (bufs[i] == NULL) { + err = -ENOMEM; + goto end; + } + memset(bufs[i], 0, size); + } + + // Test multiple target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, bufs, + frame_count, cntr_count); + if (err < 0) + goto end; + for (i = 0; i < cntr_count; ++i) { + char **target = frame_buffer; + err = memcmp(target[i], bufs[i], size); + assert(err == 0); + } + + // Test single target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, bufs, + frame_count, 1); + if (err < 0) + goto end; + for (i = 0; i < cntr_count; ++i) { + char **target = frame_buffer; + err = memcmp(target[i], bufs[i], size); + assert(err == 0); + } +end: + for (i = 0; i < cntr_count; ++i) { + if (bufs[i]) + free(bufs[i]); + } + free(bufs); + + return err; +} + +static int test_n_buf(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count) +{ + char *test_buf = frame_buffer; + unsigned int size; + char **test_vec; + int i; + int err; + + size = frame_count * snd_pcm_format_physical_width(sample_format) / 8; + + test_vec = calloc(cntr_count * 2, sizeof(*test_vec)); + if (test_vec == NULL) + return -ENOMEM; + + for (i = 0; i < cntr_count; ++i) + test_vec[i] = test_buf + size * i; + + err = test_vector(trial, access, sample_format, samples_per_frame, + frames_per_second, test_vec, frame_count, cntr_count); + free(test_vec); + + return err; +} + +static int callback(struct test_generator *gen, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, void *frame_buffer, + unsigned int frame_count) +{ + + int (*handler)(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count); + struct mapper_trial *trial = gen->private_data; + + if (access == SND_PCM_ACCESS_RW_NONINTERLEAVED) + handler = test_vector; + else if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + handler = test_n_buf; + else + handler = test_i_buf; + + return handler(trial, access, sample_format, samples_per_frame, 48000, + frame_buffer, frame_count, samples_per_frame); +}; + +int main(int argc, const char *argv[]) +{ + // Test 8/16/18/20/24/32/64 bytes per sample. + static const uint64_t sample_format_mask = + (1ull << SND_PCM_FORMAT_U8) | + (1ull << SND_PCM_FORMAT_S16_LE) | + (1ull << SND_PCM_FORMAT_S18_3LE) | + (1ull << SND_PCM_FORMAT_S20_3LE) | + (1ull << SND_PCM_FORMAT_S24_LE) | + (1ull << SND_PCM_FORMAT_S32_LE) | + (1ull << SND_PCM_FORMAT_FLOAT64_LE); + uint64_t access_mask; + struct test_generator gen = {0}; + struct mapper_trial *trial; + struct container_context *cntrs; + unsigned int samples_per_frame; + char **paths = NULL; + snd_pcm_access_t access; + bool verbose; + int i; + int err; + + // Test up to 32 channels. + samples_per_frame = 32; + cntrs = calloc(samples_per_frame, sizeof(*cntrs)); + if (cntrs == NULL) + return -ENOMEM; + + paths = calloc(samples_per_frame, sizeof(*paths)); + if (paths == NULL) { + err = -ENOMEM; + goto end; + } + for (i = 0; i < samples_per_frame; ++i) { + paths[i] = malloc(8); + if (paths[i] == NULL) { + err = -ENOMEM; + goto end; + } + snprintf(paths[i], 8, "hoge%d", i); + } + + if (argc > 1) { + char *term; + access = strtol(argv[1], &term, 10); + if (errno != 0 || *term != '\0') { + err = -EINVAL;; + goto end; + } + if (access < SND_PCM_ACCESS_MMAP_INTERLEAVED && + access > SND_PCM_ACCESS_RW_NONINTERLEAVED) { + err = -EINVAL; + goto end; + } + if (access == SND_PCM_ACCESS_MMAP_COMPLEX) { + err = -EINVAL; + goto end; + } + + access_mask = 1ull << access; + verbose = true; + } else { + access_mask = (1ull << SND_PCM_ACCESS_MMAP_INTERLEAVED) | + (1ull << SND_PCM_ACCESS_MMAP_NONINTERLEAVED) | + (1ull << SND_PCM_ACCESS_RW_INTERLEAVED) | + (1ull << SND_PCM_ACCESS_RW_NONINTERLEAVED); + verbose = false; + } + + err = generator_context_init(&gen, access_mask, sample_format_mask, + 1, samples_per_frame, + 23, 4500, 1024, + sizeof(struct mapper_trial)); + if (err < 0) + goto end; + + trial = gen.private_data; + trial->cntrs = cntrs; + trial->cntr_format = CONTAINER_FORMAT_RIFF_WAVE; + trial->paths = paths; + trial->verbose = verbose; + err = generator_context_run(&gen, callback); + + generator_context_destroy(&gen); +end: + if (paths) { + for (i = 0; i < samples_per_frame; ++i) + free(paths[i]); + free(paths); + } + free(cntrs); + + if (err < 0) { + printf("%s\n", strerror(-err)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/axfer/waiter-epoll.c b/axfer/waiter-epoll.c new file mode 100644 index 0000000..c09641b --- /dev/null +++ b/axfer/waiter-epoll.c @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter-epoll.c - Waiter for event notification by epoll(7). +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" +#include "misc.h" + +#include +#include +#include +#include +#include +#include + +struct epoll_state { + int epfd; + struct epoll_event *events; + unsigned int ev_count; +}; + +static int epoll_prepare(struct waiter_context *waiter) +{ + struct epoll_state *state = waiter->private_data; + int i; + + state->ev_count = waiter->pfd_count; + state->events = calloc(state->ev_count, sizeof(*state->events)); + if (state->events == NULL) + return -ENOMEM; + + state->epfd = epoll_create(1); + if (state->epfd < 0) + return -errno; + + for (i = 0; i < (int)waiter->pfd_count; ++i) { + struct epoll_event ev = { + .data.fd = waiter->pfds[i].fd, + .events = waiter->pfds[i].events, + }; + if (epoll_ctl(state->epfd, EPOLL_CTL_ADD, ev.data.fd, &ev) < 0) + return -errno; + } + + return 0; +} + +static int epoll_wait_event(struct waiter_context *waiter, int timeout_msec) +{ + struct epoll_state *state = waiter->private_data; + unsigned int ev_count; + int i, j; + int err; + + memset(state->events, 0, state->ev_count * sizeof(*state->events)); + err = epoll_wait(state->epfd, state->events, state->ev_count, + timeout_msec); + if (err < 0) + return -errno; + ev_count = (unsigned int)err; + + if (ev_count > 0) { + // Reconstruct data of pollfd structure. + for (i = 0; i < (int)ev_count; ++i) { + struct epoll_event *ev = &state->events[i]; + for (j = 0; j < (int)waiter->pfd_count; ++j) { + if (waiter->pfds[i].fd == ev->data.fd) { + waiter->pfds[i].revents = ev->events; + break; + } + } + } + } + + return ev_count; +} + +static void epoll_release(struct waiter_context *waiter) +{ + struct epoll_state *state = waiter->private_data; + int i; + + for (i = 0; i < (int)waiter->pfd_count; ++i) { + int fd = waiter->pfds[i].fd; + epoll_ctl(state->epfd, EPOLL_CTL_DEL, fd, NULL); + } + + free(state->events); + state->events = NULL; + + close(state->epfd); + + state->ev_count = 0; + state->epfd = 0; +} + +const struct waiter_data waiter_epoll = { + .ops = { + .prepare = epoll_prepare, + .wait_event = epoll_wait_event, + .release = epoll_release, + }, + .private_size = sizeof(struct epoll_state), +}; diff --git a/axfer/waiter-poll.c b/axfer/waiter-poll.c new file mode 100644 index 0000000..b81300c --- /dev/null +++ b/axfer/waiter-poll.c @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter-poll.c - Waiter for event notification by poll(2). +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" +#include "misc.h" + +#include +#include +#include + +static int poll_prepare(struct waiter_context *waiter ATTRIBUTE_UNUSED) +{ + // Nothing to do because an instance of waiter has required data. + return 0; +} + +static int poll_wait_event(struct waiter_context *waiter, int timeout_msec) +{ + int err; + + err = poll(waiter->pfds, waiter->pfd_count, timeout_msec); + if (err < 0) + return -errno; + + return err; +} + +static void poll_release(struct waiter_context *waiter ATTRIBUTE_UNUSED) +{ + // Nothing to do because an instance of waiter has required data. + return; +} + +const struct waiter_data waiter_poll = { + .ops = { + .prepare = poll_prepare, + .wait_event = poll_wait_event, + .release = poll_release, + }, +}; diff --git a/axfer/waiter-select.c b/axfer/waiter-select.c new file mode 100644 index 0000000..fe19776 --- /dev/null +++ b/axfer/waiter-select.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter-select.c - Waiter for event notification by select(2). +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" + +#include +#include +#include +#include +#include + +// Except for POLLERR. +#ifdef POLLRDNORM +// This program is for userspace compliant to POSIX 2008 (IEEE 1003.1:2008). +// This is the default compliance level since glibc-2.12 or later. +# define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP) +# define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT) +#else +// However it's allowed to be for old compliance levels. +# define POLLIN_SET (POLLIN | POLLHUP) +# define POLLOUT_SET (POLLOUT) +#endif +#define POLLEX_SET (POLLPRI) + + +struct select_state { + fd_set rfds_rd; + fd_set rfds_wr; + fd_set rfds_ex; +}; + +static int select_prepare(struct waiter_context *waiter ATTRIBUTE_UNUSED) +{ + return 0; +} + +static int select_wait_event(struct waiter_context *waiter, int timeout_msec) +{ + struct select_state *state = waiter->private_data; + struct pollfd *pfd; + int fd_max; + struct timeval tv, *tv_ptr; + int i; + int err; + + FD_ZERO(&state->rfds_rd); + FD_ZERO(&state->rfds_wr); + FD_ZERO(&state->rfds_ex); + + fd_max = 0; + for (i = 0; i < (int)waiter->pfd_count; ++i) { + pfd = &waiter->pfds[i]; + + if (pfd->events & POLLIN_SET) + FD_SET(pfd->fd, &state->rfds_rd); + if (pfd->events & POLLOUT_SET) + FD_SET(pfd->fd, &state->rfds_wr); + if (pfd->events & POLLEX_SET) + FD_SET(pfd->fd, &state->rfds_ex); + if (pfd->fd > fd_max) + fd_max = pfd->fd; + } + + if (timeout_msec < 0) { + tv_ptr = NULL; + } else { + tv.tv_sec = 0; + tv.tv_usec = timeout_msec * 1000; + tv_ptr = &tv; + } + + err = select(fd_max + 1, &state->rfds_rd, &state->rfds_wr, + &state->rfds_ex, tv_ptr); + if (err < 0) + return -errno; + + for (i = 0; i < (int)waiter->pfd_count; ++i) { + pfd = &waiter->pfds[i]; + + pfd->revents = 0; + if (FD_ISSET(pfd->fd, &state->rfds_rd)) + pfd->revents |= POLLIN; + if (FD_ISSET(pfd->fd, &state->rfds_wr)) + pfd->revents |= POLLOUT; + if (FD_ISSET(pfd->fd, &state->rfds_ex)) + pfd->revents |= POLLHUP; + } + + return err; +} + +static void select_release(struct waiter_context *waiter ATTRIBUTE_UNUSED) +{ + return; +} + +const struct waiter_data waiter_select = { + .ops = { + .prepare = select_prepare, + .wait_event = select_wait_event, + .release = select_release, + }, + .private_size = sizeof(struct select_state), +}; diff --git a/axfer/waiter.c b/axfer/waiter.c new file mode 100644 index 0000000..dcca028 --- /dev/null +++ b/axfer/waiter.c @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter.c - I/O event waiter. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" + +#include +#include +#include + +#include "misc.h" + +static const char *const waiter_type_labels[] = { + [WAITER_TYPE_DEFAULT] = "default", + [WAITER_TYPE_POLL] = "poll", + [WAITER_TYPE_SELECT] = "select", + [WAITER_TYPE_EPOLL] = "epoll", +}; + +enum waiter_type waiter_type_from_label(const char *label) +{ + int i; + + for (i = 0; i < (int)ARRAY_SIZE(waiter_type_labels); ++i) { + if (!strcmp(waiter_type_labels[i], label)) + return i; + } + + return WAITER_TYPE_DEFAULT; +} + +const char *waiter_label_from_type(enum waiter_type type) +{ + return waiter_type_labels[type]; +} + +int waiter_context_init(struct waiter_context *waiter, + enum waiter_type type, unsigned int pfd_count) +{ + struct { + enum waiter_type type; + const struct waiter_data *waiter; + } entries[] = { + {WAITER_TYPE_POLL, &waiter_poll}, + {WAITER_TYPE_SELECT, &waiter_select}, + {WAITER_TYPE_EPOLL, &waiter_epoll}, + }; + int i; + + if (pfd_count == 0) + return -EINVAL; + + for (i = 0; i < (int)ARRAY_SIZE(entries); ++i) { + if (entries[i].type == type) + break; + } + if (i == ARRAY_SIZE(entries)) + return -EINVAL; + + waiter->private_data = malloc(entries[i].waiter->private_size); + if (waiter->private_data == NULL) + return -ENOMEM; + memset(waiter->private_data, 0, entries[i].waiter->private_size); + + waiter->type = type; + waiter->ops = &entries[i].waiter->ops; + + waiter->pfds = calloc(pfd_count, sizeof(*waiter->pfds)); + if (waiter->pfds == NULL) + return -ENOMEM; + waiter->pfd_count = pfd_count; + + return 0; +} + +int waiter_context_prepare(struct waiter_context *waiter) +{ + return waiter->ops->prepare(waiter); +} + +int waiter_context_wait_event(struct waiter_context *waiter, + int timeout_msec) +{ + return waiter->ops->wait_event(waiter, timeout_msec); +} + +void waiter_context_release(struct waiter_context *waiter) +{ + waiter->ops->release(waiter); +} + +void waiter_context_destroy(struct waiter_context *waiter) +{ + if (waiter->pfds) + free(waiter->pfds); + waiter->pfd_count = 0; + if (waiter->private_data) + free(waiter->private_data); + waiter->private_data = NULL; +} diff --git a/axfer/waiter.h b/axfer/waiter.h new file mode 100644 index 0000000..6c4f4cb --- /dev/null +++ b/axfer/waiter.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter.h - a header for I/O event waiter. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_WAITER__H_ +#define __ALSA_UTILS_AXFER_WAITER__H_ + +#include +#include + +enum waiter_type { + WAITER_TYPE_DEFAULT = 0, + WAITER_TYPE_POLL, + WAITER_TYPE_SELECT, + WAITER_TYPE_EPOLL, + WAITER_TYPE_COUNT, +}; + +struct waiter_ops; + +struct waiter_context { + enum waiter_type type; + const struct waiter_ops *ops; + void *private_data; + + struct pollfd *pfds; + unsigned int pfd_count; +}; + +enum waiter_type waiter_type_from_label(const char *label); +const char *waiter_label_from_type(enum waiter_type type); + +int waiter_context_init(struct waiter_context *waiter, + enum waiter_type type, unsigned int pfd_count); +int waiter_context_prepare(struct waiter_context *waiter); +int waiter_context_wait_event(struct waiter_context *waiter, + int timeout_msec); +void waiter_context_release(struct waiter_context *waiter); +void waiter_context_destroy(struct waiter_context *waiter); + +// For internal use in 'waiter' module. + +struct waiter_ops { + int (*prepare)(struct waiter_context *waiter); + int (*wait_event)(struct waiter_context *waiter, int timeout_msec); + void (*release)(struct waiter_context *waiter); +}; + +struct waiter_data { + struct waiter_ops ops; + unsigned int private_size; +}; + +extern const struct waiter_data waiter_poll; +extern const struct waiter_data waiter_select; +extern const struct waiter_data waiter_epoll; + +#endif diff --git a/axfer/xfer-libasound-irq-mmap.c b/axfer/xfer-libasound-irq-mmap.c new file mode 100644 index 0000000..382ef92 --- /dev/null +++ b/axfer/xfer-libasound-irq-mmap.c @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-mmap.c - IRQ-based scheduling model for mmap operation. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" + +struct map_layout { + snd_pcm_status_t *status; + + char **vector; + unsigned int samples_per_frame; +}; + +static int irq_mmap_pre_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_access_t access; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail = 0; + int i; + int err; + + err = snd_pcm_status_malloc(&layout->status); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(state->hw_params, &access); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_channels(state->hw_params, + &layout->samples_per_frame); + if (err < 0) + return err; + + if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + layout->vector = calloc(layout->samples_per_frame, + sizeof(*layout->vector)); + if (layout->vector == NULL) + return err; + } + + if (state->verbose) { + const snd_pcm_channel_area_t *areas; + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail); + if (err < 0) + return err; + + logging(state, "attributes for mapped page frame:\n"); + for (i = 0; i < (int)layout->samples_per_frame; ++i) { + const snd_pcm_channel_area_t *area = areas + i; + + logging(state, " sample number: %d\n", i); + logging(state, " address: %p\n", area->addr); + logging(state, " bits for offset: %u\n", area->first); + logging(state, " bits/frame: %u\n", area->step); + } + logging(state, "\n"); + } + + return 0; +} + +static int irq_mmap_process_frames(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail; + unsigned int avail_count; + void *frame_buf; + snd_pcm_sframes_t consumed_count; + int err; + + if (state->use_waiter) { + unsigned int msec_per_buffer; + unsigned short revents; + + // Wait during msec equivalent to all audio data frames in + // buffer instead of period, for safe. + err = snd_pcm_hw_params_get_buffer_time(state->hw_params, + &msec_per_buffer, NULL); + if (err < 0) + return err; + msec_per_buffer /= 1000; + + // Wait for hardware IRQ when no avail space in buffer. + err = xfer_libasound_wait_event(state, msec_per_buffer, + &revents); + if (err == -ETIMEDOUT) { + logging(state, + "No event occurs for PCM substream during %u " + "msec. The implementaion of kernel driver or " + "userland backend causes this issue.\n", + msec_per_buffer); + return err; + } + if (err < 0) + return err; + if (revents & POLLERR) { + // TODO: error reporting? + return -EIO; + } + if (!(revents & (POLLIN | POLLOUT))) + return -EAGAIN; + + // When rescheduled, current position of data transmission was + // queried to actual hardware by a handler of IRQ. No need to + // perform it; e.g. ioctl(2) with SNDRV_PCM_IOCTL_HWSYNC. + } + + // Sync cache in user space to data in kernel space to calculate avail + // frames according to the latest positions on PCM buffer. + // + // This has an additional advantage to handle libasound PCM plugins. + // Most of libasound PCM plugins perform resampling in .avail_update() + // callback for capture PCM substream, then update positions on buffer. + // + // MEMO: either snd_pcm_avail_update() and snd_pcm_mmap_begin() can + // return the same number of available frames. + avail = snd_pcm_avail_update(state->handle); + if ((snd_pcm_sframes_t)avail < 0) + return (int)avail; + if (*frame_count < avail) + avail = *frame_count; + + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, &avail); + if (err < 0) + return err; + + // Trim according up to expected frame count. + if (*frame_count < avail) + avail_count = *frame_count; + else + avail_count = (unsigned int)avail; + + // TODO: Perhaps, the complex layout can be supported as a variation of + // vector type. However, there's no driver with this layout. + if (layout->vector == NULL) { + char *buf; + buf = areas[0].addr; + buf += snd_pcm_frames_to_bytes(state->handle, frame_offset); + frame_buf = buf; + } else { + int i; + for (i = 0; i < (int)layout->samples_per_frame; ++i) { + layout->vector[i] = areas[i].addr; + layout->vector[i] += snd_pcm_samples_to_bytes( + state->handle, frame_offset); + } + frame_buf = layout->vector; + } + + err = mapper_context_process_frames(mapper, frame_buf, &avail_count, + cntrs); + if (err < 0) + return err; + if (avail_count == 0) { + *frame_count = 0; + return 0; + } + + consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset, + avail_count); + if (consumed_count < 0) + return (int)consumed_count; + if (consumed_count != avail_count) + logging(state, "A bug of access plugin for this PCM node.\n"); + + *frame_count = consumed_count; + + return 0; +} + +static int irq_mmap_r_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // To querying current status of hardware, we need to care of + // synchronization between 3 levels: + // 1. status to actual hardware by driver. + // 2. status data in kernel space. + // 3. status data in user space. + // + // Kernel driver query 1 and sync 2, according to requests of some + // ioctl(2) commands. For synchronization between 2 and 3, ALSA PCM core + // supports mmap(2) operation on cache coherent architectures, some + // ioctl(2) commands on cache incoherent architecture. In usage of the + // former mechanism, we need to care of concurrent access by IRQ context + // and process context to the mapped page frame. + // In a call of ioctl(2) with SNDRV_PCM_IOCTL_STATUS and + // SNDRV_PCM_IOCTL_STATUS_EXT, the above care is needless because + // mapped page frame is unused regardless of architectures in a point of + // cache coherency. + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + // For capture direction, need to start stream explicitly. + if (s != SND_PCM_STATE_RUNNING) { + if (s != SND_PCM_STATE_PREPARED) { + err = -EPIPE; + goto error; + } + + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + } + + err = irq_mmap_process_frames(state, frame_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int irq_mmap_w_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // Read my comment in 'irq_mmap_r_process_frames(). + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + err = irq_mmap_process_frames(state, frame_count, mapper, cntrs); + if (err < 0) + goto error; + + // Need to start playback stream explicitly + if (s != SND_PCM_STATE_RUNNING) { + if (s != SND_PCM_STATE_PREPARED) { + err = -EPIPE; + goto error; + } + + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + } + + return 0; +error: + *frame_count = 0; + return err; +} + +static void irq_mmap_post_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + + if (layout->status) + snd_pcm_status_free(layout->status); + layout->status = NULL; + + free(layout->vector); + layout->vector = NULL; +} + +const struct xfer_libasound_ops xfer_libasound_irq_mmap_w_ops = { + .pre_process = irq_mmap_pre_process, + .process_frames = irq_mmap_w_process_frames, + .post_process = irq_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; + +const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops = { + .pre_process = irq_mmap_pre_process, + .process_frames = irq_mmap_r_process_frames, + .post_process = irq_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; diff --git a/axfer/xfer-libasound-irq-rw.c b/axfer/xfer-libasound-irq-rw.c new file mode 100644 index 0000000..45fb6d5 --- /dev/null +++ b/axfer/xfer-libasound-irq-rw.c @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-rw.c - IRQ-based scheduling model for read/write operation. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" +#include "frame-cache.h" + +struct rw_closure { + snd_pcm_access_t access; + int (*process_frames)(struct libasound_state *state, + snd_pcm_state_t status, unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + struct frame_cache cache; +}; + +static int wait_for_avail(struct libasound_state *state) +{ + unsigned int msec_per_buffer; + unsigned short revents; + unsigned short event; + int err; + + // Wait during msec equivalent to all audio data frames in buffer + // instead of period, for safe. + err = snd_pcm_hw_params_get_buffer_time(state->hw_params, + &msec_per_buffer, NULL); + if (err < 0) + return err; + msec_per_buffer /= 1000; + + // Wait for hardware IRQ when no available space. + err = xfer_libasound_wait_event(state, msec_per_buffer, &revents); + if (err < 0) + return err; + + // TODO: error reporting. + if (revents & POLLERR) + return -EIO; + + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + event = POLLIN; + else + event = POLLOUT; + + if (!(revents & event)) + return -EAGAIN; + + return 0; +} + +static int read_frames(struct libasound_state *state, unsigned int *frame_count, + unsigned int avail_count, struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_sframes_t handled_frame_count; + unsigned int consumed_count; + int err; + + // Trim according up to expected frame count. + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&closure->cache)) { + avail_count -= frame_cache_get_count(&closure->cache); + + // Execute write operation according to the shape of buffer. + // These operations automatically start the substream. + if (closure->access == SND_PCM_ACCESS_RW_INTERLEAVED) { + handled_frame_count = snd_pcm_readi(state->handle, + closure->cache.buf_ptr, + avail_count); + } else { + handled_frame_count = snd_pcm_readn(state->handle, + closure->cache.buf_ptr, + avail_count); + } + if (handled_frame_count < 0) { + err = handled_frame_count; + return err; + } + frame_cache_increase_count(&closure->cache, handled_frame_count); + avail_count = frame_cache_get_count(&closure->cache); + } + + // Write out to file descriptors. + consumed_count = avail_count; + err = mapper_context_process_frames(mapper, closure->cache.buf, + &consumed_count, cntrs); + if (err < 0) + return err; + + frame_cache_reduce(&closure->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int r_process_frames_blocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + snd_pcm_uframes_t avail_count; + int err = 0; + + if (status == SND_PCM_STATE_RUNNING) { + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (snd_pcm_uframes_t)avail; + + if (avail_count == 0) { + // Request data frames so that blocking is just + // released. + err = snd_pcm_sw_params_get_avail_min(state->sw_params, + &avail_count); + if (err < 0) + goto error; + } + } else { + // Request data frames so that the PCM substream starts. + snd_pcm_uframes_t frame_count; + err = snd_pcm_sw_params_get_start_threshold(state->sw_params, + &frame_count); + if (err < 0) + goto error; + + avail_count = (unsigned int)frame_count; + } + + err = read_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int r_process_frames_nonblocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + snd_pcm_uframes_t avail_count; + int err = 0; + + if (status != SND_PCM_STATE_RUNNING) { + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + } + + if (state->use_waiter) { + err = wait_for_avail(state); + if (err < 0) + goto error; + } + + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (snd_pcm_uframes_t)avail; + + if (avail_count == 0) { + // Let's go to a next iteration. + err = 0; + goto error; + } + + err = read_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int write_frames(struct libasound_state *state, + unsigned int *frame_count, unsigned int avail_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_uframes_t consumed_count; + snd_pcm_sframes_t handled_frame_count; + int err; + + // Trim according up to expected frame count. + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&closure->cache)) { + avail_count -= frame_cache_get_count(&closure->cache); + + // Read frames to transfer. + err = mapper_context_process_frames(mapper, + closure->cache.buf_ptr, &avail_count, cntrs); + if (err < 0) + return err; + frame_cache_increase_count(&closure->cache, avail_count); + avail_count = frame_cache_get_count(&closure->cache); + } + + // Execute write operation according to the shape of buffer. These + // operations automatically start the stream. + consumed_count = avail_count; + if (closure->access == SND_PCM_ACCESS_RW_INTERLEAVED) { + handled_frame_count = snd_pcm_writei(state->handle, + closure->cache.buf, consumed_count); + } else { + handled_frame_count = snd_pcm_writen(state->handle, + closure->cache.buf, consumed_count); + } + if (handled_frame_count < 0) { + err = handled_frame_count; + return err; + } + + consumed_count = handled_frame_count; + frame_cache_reduce(&closure->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int w_process_frames_blocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + unsigned int avail_count; + int err; + + if (status == SND_PCM_STATE_RUNNING) { + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (unsigned int)avail; + + if (avail_count == 0) { + // Fill with data frames so that blocking is just + // released. + snd_pcm_uframes_t avail_min; + err = snd_pcm_sw_params_get_avail_min(state->sw_params, + &avail_min); + if (err < 0) + goto error; + avail_count = (unsigned int)avail_min; + } + } else { + snd_pcm_uframes_t frames_for_start_threshold; + snd_pcm_uframes_t frames_per_period; + + // Fill with data frames so that the PCM substream starts. + err = snd_pcm_sw_params_get_start_threshold(state->sw_params, + &frames_for_start_threshold); + if (err < 0) + goto error; + + // But the above number can be too small and cause XRUN because + // I/O operation is done per period. + err = snd_pcm_hw_params_get_period_size(state->hw_params, + &frames_per_period, NULL); + if (err < 0) + goto error; + + // Use larger one to prevent from both of XRUN and successive + // blocking. + if (frames_for_start_threshold > frames_per_period) + avail_count = (unsigned int)frames_for_start_threshold; + else + avail_count = (unsigned int)frames_per_period; + } + + err = write_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int w_process_frames_nonblocking(struct libasound_state *state, + snd_pcm_state_t pcm_state ATTRIBUTE_UNUSED, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + unsigned int avail_count; + int err; + + if (state->use_waiter) { + err = wait_for_avail(state); + if (err < 0) + goto error; + } + + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (unsigned int)avail; + + if (avail_count == 0) { + // Let's go to a next iteration. + err = 0; + goto error; + } + + err = write_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + // NOTE: The substream starts automatically when the accumulated number + // of queued data frame exceeds start_threshold. + + return 0; +error: + *frame_count = 0; + return err; +} + +static int irq_rw_pre_process(struct libasound_state *state) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_format_t format; + snd_pcm_uframes_t frames_per_buffer; + int bytes_per_sample; + unsigned int samples_per_frame; + int err; + + err = snd_pcm_hw_params_get_format(state->hw_params, &format); + if (err < 0) + return err; + bytes_per_sample = snd_pcm_format_physical_width(format) / 8; + if (bytes_per_sample <= 0) + return -ENXIO; + + err = snd_pcm_hw_params_get_channels(state->hw_params, + &samples_per_frame); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_buffer_size(state->hw_params, + &frames_per_buffer); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(state->hw_params, &closure->access); + if (err < 0) + return err; + + err = frame_cache_init(&closure->cache, closure->access, + bytes_per_sample, samples_per_frame, + frames_per_buffer); + if (err < 0) + return err; + + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) { + if (state->nonblock) + closure->process_frames = r_process_frames_nonblocking; + else + closure->process_frames = r_process_frames_blocking; + } else { + if (state->nonblock) + closure->process_frames = w_process_frames_nonblocking; + else + closure->process_frames = w_process_frames_blocking; + } + + return 0; +} + +static int irq_rw_process_frames(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_state_t status; + + // Need to recover the stream. + status = snd_pcm_state(state->handle); + if (status != SND_PCM_STATE_RUNNING && status != SND_PCM_STATE_PREPARED) + return -EPIPE; + + // NOTE: Actually, status can be shift always. + return closure->process_frames(state, status, frame_count, mapper, cntrs); +} + +static void irq_rw_post_process(struct libasound_state *state) +{ + struct rw_closure *closure = state->private_data; + + frame_cache_destroy(&closure->cache); +} + +const struct xfer_libasound_ops xfer_libasound_irq_rw_ops = { + .pre_process = irq_rw_pre_process, + .process_frames = irq_rw_process_frames, + .post_process = irq_rw_post_process, + .private_size = sizeof(struct rw_closure), +}; diff --git a/axfer/xfer-libasound-timer-mmap.c b/axfer/xfer-libasound-timer-mmap.c new file mode 100644 index 0000000..cbe5c97 --- /dev/null +++ b/axfer/xfer-libasound-timer-mmap.c @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-mmap.c - Timer-based scheduling model for mmap operation. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" + +struct map_layout { + snd_pcm_status_t *status; + bool need_forward_or_rewind; + char **vector; + + unsigned int frames_per_second; + unsigned int samples_per_frame; + unsigned int frames_per_buffer; +}; + +static int timer_mmap_pre_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_access_t access; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail = 0; + snd_pcm_uframes_t frames_per_buffer; + int i; + int err; + + // This parameter, 'period event', is a software feature in alsa-lib. + // This switch a handler in 'hw' PCM plugin from irq-based one to + // timer-based one. This handler has two file descriptors for + // ALSA PCM character device and ALSA timer device. The latter is used + // to catch suspend/resume events as wakeup event. + err = snd_pcm_sw_params_set_period_event(state->handle, + state->sw_params, 1); + if (err < 0) + return err; + + err = snd_pcm_status_malloc(&layout->status); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(state->hw_params, &access); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_channels(state->hw_params, + &layout->samples_per_frame); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_rate(state->hw_params, + &layout->frames_per_second, NULL); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_buffer_size(state->hw_params, + &frames_per_buffer); + if (err < 0) + return err; + layout->frames_per_buffer = (unsigned int)frames_per_buffer; + + if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + layout->vector = calloc(layout->samples_per_frame, + sizeof(*layout->vector)); + if (layout->vector == NULL) + return err; + } + + if (state->verbose) { + const snd_pcm_channel_area_t *areas; + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail); + if (err < 0) + return err; + + logging(state, "attributes for mapped page frame:\n"); + for (i = 0; i < (int)layout->samples_per_frame; ++i) { + const snd_pcm_channel_area_t *area = areas + i; + + logging(state, " sample number: %d\n", i); + logging(state, " address: %p\n", area->addr); + logging(state, " bits for offset: %u\n", area->first); + logging(state, " bits/frame: %u\n", area->step); + } + } + + return 0; +} + +static void *get_buffer(struct libasound_state *state, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t frame_offset) +{ + struct map_layout *layout = state->private_data; + void *frame_buf; + + if (layout->vector == NULL) { + char *buf; + buf = areas[0].addr; + buf += snd_pcm_frames_to_bytes(state->handle, frame_offset); + frame_buf = buf; + } else { + int i; + for (i = 0; i < (int)layout->samples_per_frame; ++i) { + layout->vector[i] = areas[i].addr; + layout->vector[i] += snd_pcm_samples_to_bytes( + state->handle, frame_offset); + } + frame_buf = layout->vector; + } + + return frame_buf; +} + +static int timer_mmap_process_frames(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_uframes_t planned_count; + snd_pcm_sframes_t avail; + snd_pcm_uframes_t avail_count; + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t frame_offset; + void *frame_buf; + snd_pcm_sframes_t consumed_count; + int err; + + // Retrieve avail space on PCM buffer between kernel/user spaces. + // On cache incoherent architectures, still care of data + // synchronization. + avail = snd_pcm_avail_update(state->handle); + if (avail < 0) + return (int)avail; + + // Retrieve pointers of the buffer and left space up to the boundary. + avail_count = (snd_pcm_uframes_t)avail; + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail_count); + if (err < 0) + return err; + + // MEMO: Use the amount of data frames as you like. + planned_count = layout->frames_per_buffer * random() / RAND_MAX; + if (frame_offset + planned_count > layout->frames_per_buffer) + planned_count = layout->frames_per_buffer - frame_offset; + + // Trim up to expected frame count. + if (*frame_count < planned_count) + planned_count = *frame_count; + + // Yield this CPU till planned amount of frames become available. + if (avail_count < planned_count) { + unsigned short revents; + int timeout_msec; + + // TODO; precise granularity of timeout; e.g. ppoll(2). + // Furthermore, wrap up according to granularity of reported + // value for hw_ptr. + timeout_msec = ((planned_count - avail_count) * 1000 + + layout->frames_per_second - 1) / + layout->frames_per_second; + + // TODO: However, experimentally, the above is not enough to + // keep planned amount of frames when waking up. I don't know + // exactly the mechanism yet. + err = xfer_libasound_wait_event(state, timeout_msec, + &revents); + // MEMO: timeout is expected since the above call is just to measure time elapse. + if (err < 0 && err != -ETIMEDOUT) + return err; + if (revents & POLLERR) { + // TODO: error reporting. + return -EIO; + } + if (!(revents & (POLLIN | POLLOUT))) + return -EAGAIN; + + // MEMO: Need to perform hwsync explicitly because hwptr is not + // synchronized to actual position of data frame transmission + // on hardware because IRQ handlers are not used in this + // scheduling strategy. + avail = snd_pcm_avail(state->handle); + if (avail < 0) + return (int)avail; + if (avail < (snd_pcm_sframes_t)planned_count) { + logging(state, + "Wake up but not enough space: %lu %lu %u\n", + planned_count, avail, timeout_msec); + planned_count = avail; + } + } + + // Let's process data frames. + *frame_count = planned_count; + frame_buf = get_buffer(state, areas, frame_offset); + err = mapper_context_process_frames(mapper, frame_buf, frame_count, + cntrs); + if (err < 0) + return err; + + consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset, + *frame_count); + if (consumed_count != *frame_count) { + logging(state, + "A bug of 'hw' PCM plugin or driver for this PCM " + "node.\n"); + } + *frame_count = consumed_count; + + return 0; +} + +static int forward_appl_ptr(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_uframes_t forwardable_count; + snd_pcm_sframes_t forward_count; + + forward_count = snd_pcm_forwardable(state->handle); + if (forward_count < 0) + return (int)forward_count; + forwardable_count = forward_count; + + // No need to add safe-gurard because hwptr goes ahead. + forward_count = snd_pcm_forward(state->handle, forwardable_count); + if (forward_count < 0) + return (int)forward_count; + + if (state->verbose) { + logging(state, + " forwarded: %lu/%u\n", + forward_count, layout->frames_per_buffer); + } + + return 0; +} + +static int timer_mmap_r_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP suppresses any IRQ to notify + // period elapse for data transmission, therefore no need to care of + // concurrent access by IRQ context and process context, unlike + // IRQ-based operations. + // Here, this is just to query current status to hardware, for later + // processing. + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + if (s == SND_PCM_STATE_RUNNING) { + // Reduce delay between sampling on hardware and handling by + // this program. + if (layout->need_forward_or_rewind) { + err = forward_appl_ptr(state); + if (err < 0) + goto error; + layout->need_forward_or_rewind = false; + } + + err = timer_mmap_process_frames(state, frame_count, mapper, + cntrs); + if (err < 0) + goto error; + } else { + if (s == SND_PCM_STATE_PREPARED) { + // For capture direction, need to start stream + // explicitly. + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + layout->need_forward_or_rewind = true; + // Not yet. + *frame_count = 0; + } else { + err = -EPIPE; + goto error; + } + } + + return 0; +error: + *frame_count = 0; + return err; +} + +static int rewind_appl_ptr(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_uframes_t rewindable_count; + snd_pcm_sframes_t rewind_count; + + rewind_count = snd_pcm_rewindable(state->handle); + if (rewind_count < 0) + return (int)rewind_count; + rewindable_count = rewind_count; + + // If appl_ptr were rewound just to position of hw_ptr, at next time, + // hw_ptr could catch up appl_ptr. This is overrun. We need a space + // between these two pointers to prevent this XRUN. + // This space is largely affected by time to process data frames later. + // + // TODO: a generous way to estimate a good value. + if (rewindable_count < 32) + return 0; + rewindable_count -= 32; + + rewind_count = snd_pcm_rewind(state->handle, rewindable_count); + if (rewind_count < 0) + return (int)rewind_count; + + if (state->verbose) { + logging(state, + " rewound: %lu/%u\n", + rewind_count, layout->frames_per_buffer); + } + + return 0; +} + +static int fill_buffer_with_zero_samples(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail_count; + snd_pcm_format_t sample_format; + snd_pcm_uframes_t consumed_count; + int err; + + err = snd_pcm_hw_params_get_buffer_size(state->hw_params, + &avail_count); + if (err < 0) + return err; + + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail_count); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_format(state->hw_params, &sample_format); + if (err < 0) + return err; + + err = snd_pcm_areas_silence(areas, frame_offset, + layout->samples_per_frame, avail_count, + sample_format); + if (err < 0) + return err; + + consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset, + avail_count); + if (consumed_count != avail_count) + logging(state, "A bug of access plugin for this PCM node.\n"); + + return 0; +} + +static int timer_mmap_w_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // Read my comment in 'timer_mmap_w_process_frames()'. + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + if (s == SND_PCM_STATE_RUNNING) { + // Reduce delay between queueing by this program and presenting + // on hardware. + if (layout->need_forward_or_rewind) { + err = rewind_appl_ptr(state); + if (err < 0) + goto error; + layout->need_forward_or_rewind = false; + } + + err = timer_mmap_process_frames(state, frame_count, mapper, + cntrs); + if (err < 0) + goto error; + } else { + // Need to start playback stream explicitly + if (s == SND_PCM_STATE_PREPARED) { + err = fill_buffer_with_zero_samples(state); + if (err < 0) + goto error; + + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + + layout->need_forward_or_rewind = true; + // Not yet. + *frame_count = 0; + } else { + err = -EPIPE; + goto error; + } + } + + return 0; +error: + *frame_count = 0; + return err; +} + +static void timer_mmap_post_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + + if (layout->status) + snd_pcm_status_free(layout->status); + layout->status = NULL; + + if (layout->vector) + free(layout->vector); + layout->vector = NULL; +} + +const struct xfer_libasound_ops xfer_libasound_timer_mmap_w_ops = { + .pre_process = timer_mmap_pre_process, + .process_frames = timer_mmap_w_process_frames, + .post_process = timer_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; + +const struct xfer_libasound_ops xfer_libasound_timer_mmap_r_ops = { + .pre_process = timer_mmap_pre_process, + .process_frames = timer_mmap_r_process_frames, + .post_process = timer_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c new file mode 100644 index 0000000..36ee08d --- /dev/null +++ b/axfer/xfer-libasound.c @@ -0,0 +1,934 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound.c - receive/transmit frames by alsa-lib. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" + +static const char *const sched_model_labels [] = { + [SCHED_MODEL_IRQ] = "irq", + [SCHED_MODEL_TIMER] = "timer", +}; + +enum no_short_opts { + // 200 or later belong to non us-ascii character set. + OPT_PERIOD_SIZE = 200, + OPT_BUFFER_SIZE, + OPT_WAITER_TYPE, + OPT_SCHED_MODEL, + OPT_DISABLE_RESAMPLE, + OPT_DISABLE_CHANNELS, + OPT_DISABLE_FORMAT, + OPT_DISABLE_SOFTVOL, + OPT_FATAL_ERRORS, + OPT_TEST_NOWAIT, + // Obsoleted. + OPT_TEST_POSITION, + OPT_TEST_COEF, +}; + +#define S_OPTS "D:NMF:B:A:R:T:m:" +static const struct option l_opts[] = { + {"device", 1, 0, 'D'}, + {"nonblock", 0, 0, 'N'}, + {"mmap", 0, 0, 'M'}, + {"period-time", 1, 0, 'F'}, + {"buffer-time", 1, 0, 'B'}, + {"period-size", 1, 0, OPT_PERIOD_SIZE}, + {"buffer-size", 1, 0, OPT_BUFFER_SIZE}, + {"avail-min", 1, 0, 'A'}, + {"start-delay", 1, 0, 'R'}, + {"stop-delay", 1, 0, 'T'}, + {"waiter-type", 1, 0, OPT_WAITER_TYPE}, + {"sched-model", 1, 0, OPT_SCHED_MODEL}, + // For plugins in alsa-lib. + {"disable-resample", 0, 0, OPT_DISABLE_RESAMPLE}, + {"disable-channels", 0, 0, OPT_DISABLE_CHANNELS}, + {"disable-format", 0, 0, OPT_DISABLE_FORMAT}, + {"disable-softvol", 0, 0, OPT_DISABLE_SOFTVOL}, + // For debugging. + {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, + {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, + // Obsoleted. + {"chmap", 1, 0, 'm'}, + {"test-position", 0, 0, OPT_TEST_POSITION}, + {"test-coef", 1, 0, OPT_TEST_COEF}, +}; + +static int xfer_libasound_init(struct xfer_context *xfer, + snd_pcm_stream_t stream ATTRIBUTE_UNUSED) +{ + struct libasound_state *state = xfer->private_data; + int err; + + err = snd_output_stdio_attach(&state->log, stderr, 0); + if (err < 0) + return err; + + err = snd_pcm_hw_params_malloc(&state->hw_params); + if (err < 0) + return err; + + return snd_pcm_sw_params_malloc(&state->sw_params); +} + +static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, + const char *optarg) +{ + struct libasound_state *state = xfer->private_data; + int err = 0; + + if (key == 'D') + state->node_literal = arg_duplicate_string(optarg, &err); + else if (key == 'N') + state->nonblock = true; + else if (key == 'M') + state->mmap = true; + else if (key == 'F') + state->msec_per_period = arg_parse_decimal_num(optarg, &err); + else if (key == 'B') + state->msec_per_buffer = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_PERIOD_SIZE) + state->frames_per_period = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_BUFFER_SIZE) + state->frames_per_buffer = arg_parse_decimal_num(optarg, &err); + else if (key == 'A') + state->msec_for_avail_min = arg_parse_decimal_num(optarg, &err); + else if (key == 'R') + state->msec_for_start_threshold = arg_parse_decimal_num(optarg, &err); + else if (key == 'T') + state->msec_for_stop_threshold = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_WAITER_TYPE) + state->waiter_type_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_SCHED_MODEL) + state->sched_model_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_DISABLE_RESAMPLE) + state->no_auto_resample = true; + else if (key == OPT_DISABLE_CHANNELS) + state->no_auto_channels = true; + else if (key == OPT_DISABLE_FORMAT) + state->no_auto_format = true; + else if (key == OPT_DISABLE_SOFTVOL) + state->no_softvol = true; + else if (key == 'm' || + key == OPT_TEST_POSITION || + key == OPT_TEST_COEF) + err = -EINVAL; + else if (key == OPT_FATAL_ERRORS) + state->finish_at_xrun = true; + else if (key == OPT_TEST_NOWAIT) + state->test_nowait = true; + else + err = -ENXIO; + + return err; +} + +int xfer_libasound_validate_opts(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + int err = 0; + + state->verbose = xfer->verbose > 1; + + if (state->node_literal == NULL) { + state->node_literal = strdup("default"); + if (state->node_literal == NULL) + return -ENOMEM; + } + + if (state->mmap && state->nonblock) { + fprintf(stderr, + "An option for mmap operation should not be used with " + "nonblocking option.\n"); + return -EINVAL; + } + + if (state->test_nowait) { + if (!state->nonblock && !state->mmap) { + fprintf(stderr, + "An option for nowait test should be used with " + "nonblock or mmap options.\n"); + return -EINVAL; + } + } + + if (state->msec_per_period > 0 && state->msec_per_buffer > 0) { + if (state->msec_per_period > state->msec_per_buffer) { + state->msec_per_period = state->msec_per_buffer; + state->msec_per_buffer = 0; + } + } + + if (state->frames_per_period > 0 && state->frames_per_buffer > 0) { + if (state->frames_per_period > state->frames_per_buffer) { + state->frames_per_period = state->frames_per_buffer; + state->frames_per_buffer = 0; + } + } + + state->sched_model = SCHED_MODEL_IRQ; + if (state->sched_model_literal != NULL) { + if (!strcmp(state->sched_model_literal, "timer")) { + state->sched_model = SCHED_MODEL_TIMER; + state->mmap = true; + state->nonblock = true; + } + } + + if (state->waiter_type_literal != NULL) { + if (state->test_nowait) { + fprintf(stderr, + "An option for waiter type should not be " + "used with nowait test option.\n"); + return -EINVAL; + } + if (!state->nonblock && !state->mmap) { + fprintf(stderr, + "An option for waiter type should be used " + "with nonblock or mmap or timer-based " + "scheduling options.\n"); + return -EINVAL; + } + state->waiter_type = + waiter_type_from_label(state->waiter_type_literal); + } else { + state->waiter_type = WAITER_TYPE_DEFAULT; + } + + return err; +} + +static int set_access_hw_param(struct libasound_state *state) +{ + snd_pcm_access_mask_t *mask; + int err; + + err = snd_pcm_access_mask_malloc(&mask); + if (err < 0) + return err; + snd_pcm_access_mask_none(mask); + if (state->mmap) { + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_INTERLEAVED); + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED); + } else { + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_RW_NONINTERLEAVED); + } + err = snd_pcm_hw_params_set_access_mask(state->handle, state->hw_params, + mask); + snd_pcm_access_mask_free(mask); + + return err; +} + +static int disable_period_wakeup(struct libasound_state *state) +{ + int err; + + if (snd_pcm_type(state->handle) != SND_PCM_TYPE_HW) { + logging(state, + "Timer-based scheduling is only available for 'hw' " + "PCM plugin.\n"); + return -ENXIO; + } + + if (!snd_pcm_hw_params_can_disable_period_wakeup(state->hw_params)) { + logging(state, + "This hardware doesn't support the mode of no-period-" + "wakeup. In this case, timer-based scheduling is not " + "available.\n"); + return -EIO; + } + + err = snd_pcm_hw_params_set_period_wakeup(state->handle, + state->hw_params, 0); + if (err < 0) { + logging(state, + "Fail to disable period wakeup so that the hardware " + "generates no IRQs during transmission of data " + "frames.\n"); + } + + return err; +} + +static int open_handle(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + int mode = 0; + int err; + + if (state->nonblock) + mode |= SND_PCM_NONBLOCK; + if (state->no_auto_resample) + mode |= SND_PCM_NO_AUTO_RESAMPLE; + if (state->no_auto_channels) + mode |= SND_PCM_NO_AUTO_CHANNELS; + if (state->no_auto_format) + mode |= SND_PCM_NO_AUTO_FORMAT; + if (state->no_softvol) + mode |= SND_PCM_NO_SOFTVOL; + + err = snd_pcm_open(&state->handle, state->node_literal, xfer->direction, + mode); + if (err < 0) { + logging(state, "Fail to open libasound PCM node for %s: %s\n", + snd_pcm_stream_name(xfer->direction), + state->node_literal); + return err; + } + + if ((state->nonblock || state->mmap) && !state->test_nowait) + state->use_waiter = true; + + err = snd_pcm_hw_params_any(state->handle, state->hw_params); + if (err < 0) + return err; + + if (state->sched_model == SCHED_MODEL_TIMER) { + err = disable_period_wakeup(state); + if (err < 0) + return err; + } + + if (xfer->dump_hw_params) { + logging(state, "Available HW Params of node: %s\n", + snd_pcm_name(state->handle)); + snd_pcm_hw_params_dump(state->hw_params, state->log); + // TODO: there're more parameters which are not dumped by + // alsa-lib. + return 0; + } + + return set_access_hw_param(state); +} + +static int prepare_waiter(struct libasound_state *state) +{ + unsigned int pfd_count; + int err; + + // Nothing to do for dafault waiter (=snd_pcm_wait()). + if (state->waiter_type == WAITER_TYPE_DEFAULT) + return 0; + + err = snd_pcm_poll_descriptors_count(state->handle); + if (err < 0) + return err; + if (err == 0) + return -ENXIO; + pfd_count = (unsigned int)err; + + state->waiter = malloc(sizeof(*state->waiter)); + if (state->waiter == NULL) + return -ENOMEM; + + err = waiter_context_init(state->waiter, state->waiter_type, pfd_count); + if (err < 0) + return err; + + err = snd_pcm_poll_descriptors(state->handle, state->waiter->pfds, + pfd_count); + if (err < 0) + return err; + + return waiter_context_prepare(state->waiter); +} + +int xfer_libasound_wait_event(struct libasound_state *state, int timeout_msec, + unsigned short *revents) +{ + int count; + + if (state->waiter_type != WAITER_TYPE_DEFAULT) { + struct waiter_context *waiter = state->waiter; + int err; + + count = waiter_context_wait_event(waiter, timeout_msec); + if (count < 0) + return count; + if (count == 0 && timeout_msec > 0) + return -ETIMEDOUT; + + err = snd_pcm_poll_descriptors_revents(state->handle, + waiter->pfds, waiter->pfd_count, revents); + if (err < 0) + return err; + } else { + count = snd_pcm_wait(state->handle, timeout_msec); + if (count < 0) + return count; + if (count == 0 && timeout_msec > 0) + return -ETIMEDOUT; + + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_PLAYBACK) + *revents = POLLOUT; + else + *revents = POLLIN; + } + + return 0; +} + +static int configure_hw_params(struct libasound_state *state, + snd_pcm_format_t format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + unsigned int msec_per_period, + unsigned int msec_per_buffer, + snd_pcm_uframes_t frames_per_period, + snd_pcm_uframes_t frames_per_buffer) +{ + int err; + + // Configure sample format. + if (format == SND_PCM_FORMAT_UNKNOWN) { + snd_pcm_format_mask_t *mask; + + err = snd_pcm_format_mask_malloc(&mask); + if (err < 0) + return err; + snd_pcm_hw_params_get_format_mask(state->hw_params, mask); + for (format = 0; format <= SND_PCM_FORMAT_LAST; ++format) { + if (snd_pcm_format_mask_test(mask, format)) + break; + } + snd_pcm_format_mask_free(mask); + if (format > SND_PCM_FORMAT_LAST) { + logging(state, + "Any sample format is not available.\n"); + return -EINVAL; + } + } + err = snd_pcm_hw_params_set_format(state->handle, state->hw_params, + format); + if (err < 0) { + logging(state, + "Sample format '%s' is not available: %s\n", + snd_pcm_format_name(format), snd_strerror(err)); + return err; + } + + // Configure channels. + if (samples_per_frame == 0) { + err = snd_pcm_hw_params_get_channels_min(state->hw_params, + &samples_per_frame); + if (err < 0) { + logging(state, + "Any channel number is not available.\n"); + return err; + } + } + err = snd_pcm_hw_params_set_channels(state->handle, state->hw_params, + samples_per_frame); + if (err < 0) { + logging(state, + "Channels count '%u' is not available: %s\n", + samples_per_frame, snd_strerror(err)); + return err; + } + + // Configure rate. + if (frames_per_second == 0) { + err = snd_pcm_hw_params_get_rate_min(state->hw_params, + &frames_per_second, NULL); + if (err < 0) { + logging(state, + "Any rate is not available.\n"); + return err; + } + + } + err = snd_pcm_hw_params_set_rate(state->handle, state->hw_params, + frames_per_second, 0); + if (err < 0) { + logging(state, + "Sampling rate '%u' is not available: %s\n", + frames_per_second, snd_strerror(err)); + return err; + } + + // Keep one of 'frames_per_buffer' and 'msec_per_buffer'. + if (frames_per_buffer == 0) { + if (msec_per_buffer == 0) { + err = snd_pcm_hw_params_get_buffer_time_max( + state->hw_params, &msec_per_buffer, NULL); + if (err < 0) { + logging(state, + "The maximum msec per buffer is not " + "available.\n"); + return err; + } + if (msec_per_buffer > 500000) + msec_per_buffer = 500000; + } + } else if (msec_per_buffer > 0) { + uint64_t msec; + + msec = 1000000 * frames_per_buffer / frames_per_second; + if (msec < msec_per_buffer) + msec_per_buffer = 0; + } + + // Keep one of 'frames_per_period' and 'msec_per_period'. + if (frames_per_period == 0) { + if (msec_per_period == 0) { + if (msec_per_buffer > 0) + msec_per_period = msec_per_buffer / 4; + else + frames_per_period = frames_per_buffer / 4; + } + } else if (msec_per_period > 0) { + uint64_t msec; + + msec = 1000000 * frames_per_period / frames_per_second; + if (msec < msec_per_period) + msec_per_period = 0; + } + + if (msec_per_period) { + err = snd_pcm_hw_params_set_period_time_near(state->handle, + state->hw_params, &msec_per_period, NULL); + if (err < 0) { + logging(state, + "Fail to configure period time: %u msec\n", + msec_per_period); + return err; + } + } else { + err = snd_pcm_hw_params_set_period_size_near(state->handle, + state->hw_params, &frames_per_period, NULL); + if (err < 0) { + logging(state, + "Fail to configure period size: %lu frames\n", + frames_per_period); + return err; + } + } + + if (msec_per_buffer) { + err = snd_pcm_hw_params_set_buffer_time_near(state->handle, + state->hw_params, &msec_per_buffer, NULL); + if (err < 0) { + logging(state, + "Fail to configure buffer time: %u msec\n", + msec_per_buffer); + return err; + } + } else { + err = snd_pcm_hw_params_set_buffer_size_near(state->handle, + state->hw_params, &frames_per_buffer); + if (err < 0) { + logging(state, + "Fail to configure buffer size: %lu frames\n", + frames_per_buffer); + return err; + } + } + + return snd_pcm_hw_params(state->handle, state->hw_params); +} + +static int retrieve_actual_hw_params(snd_pcm_hw_params_t *hw_params, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer) +{ + int err; + + err = snd_pcm_hw_params_get_format(hw_params, format); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_channels(hw_params, + samples_per_frame); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_rate(hw_params, frames_per_second, + NULL); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(hw_params, access); + if (err < 0) + return err; + + return snd_pcm_hw_params_get_buffer_size(hw_params, frames_per_buffer); +} + +static int configure_sw_params(struct libasound_state *state, + unsigned int frames_per_second, + unsigned int frames_per_buffer, + unsigned int msec_for_avail_min, + unsigned int msec_for_start_threshold, + unsigned int msec_for_stop_threshold) +{ + snd_pcm_uframes_t frame_count; + int err; + + if (msec_for_avail_min > 0) { + frame_count = msec_for_avail_min * frames_per_second / 1000000; + if (frame_count == 0 || frame_count > frames_per_buffer) { + logging(state, + "The msec for 'avail_min' is too %s: %u " + "msec (%lu frames at %u).\n", + frame_count == 0 ? "small" : "large", + msec_for_avail_min, frame_count, + frames_per_second); + return -EINVAL; + } + err = snd_pcm_sw_params_set_avail_min(state->handle, + state->sw_params, frame_count); + if (err < 0) { + logging(state, + "Fail to configure 'avail-min'.\n"); + return -EINVAL; + } + } + + if (msec_for_start_threshold > 0) { + frame_count = msec_for_start_threshold * frames_per_second / + 1000000; + if (frame_count == 0 || frame_count > frames_per_buffer) { + logging(state, + "The msec for 'start-delay' is too %s: %u " + "msec (%lu frames at %u).\n", + frame_count == 0 ? "small" : "large", + msec_for_start_threshold, frame_count, + frames_per_second); + return -EINVAL; + } + err = snd_pcm_sw_params_set_start_threshold(state->handle, + state->sw_params, frame_count); + if (err < 0) { + logging(state, + "Fail to configure 'start-delay'.\n"); + return -EINVAL; + } + } + + if (msec_for_stop_threshold > 0) { + frame_count = msec_for_stop_threshold * frames_per_second / + 1000000; + if (frame_count == 0 || frame_count > frames_per_buffer) { + logging(state, + "The msec for 'stop-delay' is too %s: %u " + "msec (%lu frames at %u).\n", + frame_count == 0 ? "small" : "large", + msec_for_stop_threshold, frame_count, + frames_per_second); + return -EINVAL; + } + err = snd_pcm_sw_params_set_stop_threshold(state->handle, + state->sw_params, frame_count); + if (err < 0) { + logging(state, + "Fail to configure 'stop-delay'.\n"); + return -EINVAL; + } + } + + return snd_pcm_sw_params(state->handle, state->sw_params); +} + +static int xfer_libasound_pre_process(struct xfer_context *xfer, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer) +{ + struct libasound_state *state = xfer->private_data; + unsigned int flag; + int err; + + err = open_handle(xfer); + if (err < 0) + return -ENXIO; + + err = configure_hw_params(state, *format, *samples_per_frame, + *frames_per_second, + state->msec_per_period, + state->msec_per_buffer, + state->frames_per_period, + state->frames_per_buffer); + if (err < 0) { + logging(state, "Current hardware parameters:\n"); + snd_pcm_hw_params_dump(state->hw_params, state->log); + return err; + } + + // Retrieve actual parameters. + err = retrieve_actual_hw_params(state->hw_params, format, + samples_per_frame, frames_per_second, + access, frames_per_buffer); + if (err < 0) + return err; + + // Query software parameters. + err = snd_pcm_sw_params_current(state->handle, state->sw_params); + if (err < 0) + return err; + + // Assign I/O operation. + err = snd_pcm_hw_params_get_period_wakeup(state->handle, + state->hw_params, &flag); + if (err < 0) + return err; + + if (flag) { + if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || + *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { + state->ops = &xfer_libasound_irq_rw_ops; + } else if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_irq_mmap_r_ops; + else + state->ops = &xfer_libasound_irq_mmap_w_ops; + } else { + return -ENXIO; + } + } else { + if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_timer_mmap_r_ops; + else + state->ops = &xfer_libasound_timer_mmap_w_ops; + } else { + return -ENXIO; + } + } + + if (state->ops->private_size > 0) { + state->private_data = malloc(state->ops->private_size); + if (state->private_data == NULL) + return -ENOMEM; + memset(state->private_data, 0, state->ops->private_size); + } + + err = state->ops->pre_process(state); + if (err < 0) + return err; + + err = configure_sw_params(state, *frames_per_second, + *frames_per_buffer, + state->msec_for_avail_min, + state->msec_for_start_threshold, + state->msec_for_stop_threshold); + if (err < 0) { + logging(state, "Current software parameters:\n"); + snd_pcm_sw_params_dump(state->sw_params, state->log); + return err; + } + + if (xfer->verbose > 0) { + snd_pcm_dump(state->handle, state->log); + logging(state, "Scheduling model:\n"); + logging(state, " %s\n", sched_model_labels[state->sched_model]); + } + + if (state->use_waiter) { + // NOTE: This should be after configuring sw_params due to + // timer descriptor for time-based scheduling model. + err = prepare_waiter(state); + if (err < 0) + return err; + + if (xfer->verbose > 0) { + logging(state, "Waiter type:\n"); + logging(state, + " %s\n", + waiter_label_from_type(state->waiter_type)); + } + } + + return 0; +} + +static int xfer_libasound_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libasound_state *state = xfer->private_data; + int err; + + if (state->handle == NULL) + return -ENXIO; + + err = state->ops->process_frames(state, frame_count, mapper, cntrs); + if (err < 0) { + if (err == -EAGAIN) + return err; + if (err == -EPIPE && !state->finish_at_xrun) { + // Recover the stream and continue processing + // immediately. In this program -EPIPE comes from + // libasound implementation instead of file I/O. + err = snd_pcm_prepare(state->handle); + } + + if (err < 0) { + // TODO: -EIO from libasound for hw PCM node means + // that IRQ disorder. This should be reported to help + // developers for drivers. + logging(state, "Fail to process frames: %s\n", + snd_strerror(err)); + } + } + + return err; +} + +static void xfer_libasound_pause(struct xfer_context *xfer, bool enable) +{ + struct libasound_state *state = xfer->private_data; + snd_pcm_state_t s = snd_pcm_state(state->handle); + int err; + + if (state->handle == NULL) + return; + + if (enable) { + if (s != SND_PCM_STATE_RUNNING) + return; + } else { + if (s != SND_PCM_STATE_PAUSED) + return; + } + + // Not supported. Leave the substream to enter XRUN state. + if (!snd_pcm_hw_params_can_pause(state->hw_params)) + return; + + err = snd_pcm_pause(state->handle, enable); + if (err < 0 && state->verbose) { + logging(state, "snd_pcm_pause(): %s\n", snd_strerror(err)); + } +} + +static void xfer_libasound_post_process(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + snd_pcm_state_t pcm_state; + int err; + + if (state->handle == NULL) + return; + + pcm_state = snd_pcm_state(state->handle); + if (pcm_state != SND_PCM_STATE_OPEN && + pcm_state != SND_PCM_STATE_DISCONNECTED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE || + state->ops == &xfer_libasound_timer_mmap_w_ops) { + err = snd_pcm_drop(state->handle); + if (err < 0) + logging(state, "snd_pcm_drop(): %s\n", + snd_strerror(err)); + } else { + // TODO: this is a bug in kernel land. + if (state->nonblock) + snd_pcm_nonblock(state->handle, 0); + err = snd_pcm_drain(state->handle); + if (state->nonblock) + snd_pcm_nonblock(state->handle, 1); + if (err < 0) + logging(state, "snd_pcm_drain(): %s\n", + snd_strerror(err)); + } + } + + err = snd_pcm_hw_free(state->handle); + if (err < 0) + logging(state, "snd_pcm_hw_free(): %s\n", snd_strerror(err)); + + snd_pcm_close(state->handle); + state->handle = NULL; + + if (state->ops && state->ops->post_process) + state->ops->post_process(state); + free(state->private_data); + state->private_data = NULL; + + // Free cache of content for configuration files so that memory leaks + // are not detected. + snd_config_update_free_global(); +} + +static void xfer_libasound_destroy(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + + free(state->node_literal); + free(state->waiter_type_literal); + free(state->sched_model_literal); + state->node_literal = NULL; + state->waiter_type_literal = NULL; + state->sched_model_literal = NULL; + + if (state->hw_params) + snd_pcm_hw_params_free(state->hw_params); + if (state->sw_params) + snd_pcm_sw_params_free(state->sw_params); + state->hw_params = NULL; + state->sw_params = NULL; + + if (state->log) + snd_output_close(state->log); + state->log = NULL; +} + +static void xfer_libasound_help(struct xfer_context *xfer ATTRIBUTE_UNUSED) +{ + printf( +" [BASICS]\n" +" -D, --device select node by name in coniguration space\n" +" -N, --nonblock nonblocking mode\n" +" -M, --mmap use mmap(2) for zero copying technique\n" +" -F, --period-time interval between interrupts (msec unit)\n" +" --period-size interval between interrupts (frame unit)\n" +" -B, --buffer-time size of buffer for frame(msec unit)\n" +" --buffer-size size of buffer for frame(frame unit)\n" +" --waiter-type type of waiter to handle available frames\n" +" --sched-model model of process scheduling\n" +" [SOFTWARE FEATURES]\n" +" -A, --avail-min threshold of frames to wake up process\n" +" -R, --start-delay threshold of frames to start PCM substream\n" +" -T, --stop-delay threshold of frames to stop PCM substream\n" +" [LIBASOUND PLUGIN OPTIONS]\n" +" --disable-resample disable rate conversion for plug plugin\n" +" --disable-channels disable channel conversion for plug plugin\n" +" --disable-format disable format conversion for plug plugin\n" +" --disable-softvol disable software volume for sofvol plugin\n" +" [DEBUG ASSISTANT]\n" +" --fatal-errors finish at XRUN\n" +" --test-nowait busy poll without any waiter\n" + ); +} + +const struct xfer_data xfer_libasound = { + .s_opts = S_OPTS, + .l_opts = l_opts, + .l_opts_count = ARRAY_SIZE(l_opts), + .ops = { + .init = xfer_libasound_init, + .parse_opt = xfer_libasound_parse_opt, + .validate_opts = xfer_libasound_validate_opts, + .pre_process = xfer_libasound_pre_process, + .process_frames = xfer_libasound_process_frames, + .pause = xfer_libasound_pause, + .post_process = xfer_libasound_post_process, + .destroy = xfer_libasound_destroy, + .help = xfer_libasound_help, + }, + .private_size = sizeof(struct libasound_state), +}; diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h new file mode 100644 index 0000000..dfe8577 --- /dev/null +++ b/axfer/xfer-libasound.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound.h - a header for receiver/transmitter of frames by alsa-lib. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_XFER_LIBASOUND__H_ +#define __ALSA_UTILS_AXFER_XFER_LIBASOUND__H_ + +#include "xfer.h" +#include "waiter.h" + +#define logging(state, ...) \ + snd_output_printf(state->log, __VA_ARGS__) + +enum sched_model { + SCHED_MODEL_IRQ = 0, + SCHED_MODEL_TIMER, + SCHED_MODEL_COUNT, +}; + +struct xfer_libasound_ops; + +struct libasound_state { + snd_pcm_t *handle; + + snd_output_t *log; + snd_pcm_hw_params_t *hw_params; + snd_pcm_sw_params_t *sw_params; + + const struct xfer_libasound_ops *ops; + void *private_data; + + bool verbose; + + char *node_literal; + char *waiter_type_literal; + char *sched_model_literal; + + unsigned int msec_per_period; + unsigned int msec_per_buffer; + unsigned int frames_per_period; + unsigned int frames_per_buffer; + + unsigned int msec_for_avail_min; + unsigned int msec_for_start_threshold; + unsigned int msec_for_stop_threshold; + + bool finish_at_xrun:1; + bool nonblock:1; + bool mmap:1; + bool test_nowait:1; + bool no_auto_resample:1; + bool no_auto_channels:1; + bool no_auto_format:1; + bool no_softvol:1; + + bool use_waiter:1; + + enum waiter_type waiter_type; + struct waiter_context *waiter; + + // For scheduling type. + enum sched_model sched_model; +}; + +// For internal use in 'libasound' module. + +struct xfer_libasound_ops { + int (*pre_process)(struct libasound_state *state); + int (*process_frames)(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + void (*post_process)(struct libasound_state *state); + unsigned int private_size; +}; + +int xfer_libasound_wait_event(struct libasound_state *state, int timeout_msec, + unsigned short *revents); + +extern const struct xfer_libasound_ops xfer_libasound_irq_rw_ops; + +extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops; +extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_w_ops; + +extern const struct xfer_libasound_ops xfer_libasound_timer_mmap_w_ops; +extern const struct xfer_libasound_ops xfer_libasound_timer_mmap_r_ops; + +#endif diff --git a/axfer/xfer-libffado.c b/axfer/xfer-libffado.c new file mode 100644 index 0000000..6db835d --- /dev/null +++ b/axfer/xfer-libffado.c @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libffado.c - receive/transmit frames by libffado. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer.h" +#include "misc.h" + +#include "frame-cache.h" + +#include +#include + +#include + +struct libffado_state { + ffado_device_t *handle; + enum ffado_direction direction; + + char *port_literal; + char *node_literal; + char *guid_literal; + unsigned int frames_per_period; + unsigned int periods_per_buffer; + unsigned int sched_priority; + bool slave_mode:1; + bool snoop_mode:1; + + unsigned int data_ch_count; + ffado_streaming_stream_type *data_ch_map; + + int (*process_frames)(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + + struct frame_cache cache; +}; + +enum no_short_opts { + OPT_FRAMES_PER_PERIOD = 200, + OPT_PERIODS_PER_BUFFER, + OPT_SLAVE_MODE, + OPT_SNOOP_MODE, + OPT_SCHED_PRIORITY, +}; + +#define S_OPTS "p:n:g:" +static const struct option l_opts[] = { + {"port", 1, 0, 'p'}, + {"node", 1, 0, 'n'}, + {"guid", 1, 0, 'g'}, + {"frames-per-period", 1, 0, OPT_FRAMES_PER_PERIOD}, + {"periods-per-buffer", 1, 0, OPT_PERIODS_PER_BUFFER}, + {"slave", 0, 0, OPT_SLAVE_MODE}, + {"snoop", 0, 0, OPT_SNOOP_MODE}, + {"sched-priority", 1, 0, OPT_SCHED_PRIORITY}, // to SCHED_FIFO +}; + +static int xfer_libffado_init(struct xfer_context *xfer, + snd_pcm_stream_t direction) +{ + struct libffado_state *state = xfer->private_data; + + if (direction == SND_PCM_STREAM_CAPTURE) + state->direction = FFADO_CAPTURE; + else if (direction == SND_PCM_STREAM_PLAYBACK) + state->direction = FFADO_PLAYBACK; + else + return -EINVAL; + + return 0; +} + +static int xfer_libffado_parse_opt(struct xfer_context *xfer, int key, + const char *optarg) +{ + struct libffado_state *state = xfer->private_data; + int err; + + if (key == 'p') + state->port_literal = arg_duplicate_string(optarg, &err); + else if (key == 'n') + state->node_literal = arg_duplicate_string(optarg, &err); + else if (key == 'g') + state->guid_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_FRAMES_PER_PERIOD) + state->frames_per_period = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_PERIODS_PER_BUFFER) + state->periods_per_buffer = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_SLAVE_MODE) + state->slave_mode = true; + else if (key == OPT_SNOOP_MODE) + state->snoop_mode = true; + else if (key == OPT_SCHED_PRIORITY) + state->sched_priority = arg_parse_decimal_num(optarg, &err); + else + err = -ENXIO; + + return err; +} + +static int validate_sched_priority(struct libffado_state *state) +{ + int val; + + val = sched_get_priority_max(SCHED_FIFO); + if (val < 0) + return -errno; + if (state->sched_priority > val) + return -EINVAL; + + val = sched_get_priority_min(SCHED_FIFO); + if (val < 0) + return -errno; + if (state->sched_priority < val) + return -EINVAL; + + return 0; +} + +static int xfer_libffado_validate_opts(struct xfer_context *xfer) +{ + struct libffado_state *state = xfer->private_data; + int err; + + if (state->node_literal != NULL) { + if (state->port_literal == NULL) { + fprintf(stderr, + "'n' option should correspond 'p' option.\n"); + return -EINVAL; + } + } + + if (state->port_literal != NULL && state->guid_literal != NULL) { + fprintf(stderr, + "Neither 'p' option nor 'g' option is available at the " + "same time.\n"); + return -EINVAL; + } + + if (state->guid_literal != NULL) { + if (strstr(state->guid_literal, "0x") != state->guid_literal) { + fprintf(stderr, + "A value of 'g' option should have '0x' as its " + "prefix for hexadecimal number.\n"); + return -EINVAL; + } + } + + if (state->slave_mode && state->snoop_mode) { + fprintf(stderr, "Neither slave mode nor snoop mode is available" + "at the same time.\n"); + return -EINVAL; + } + + if (state->sched_priority > 0) { + err = validate_sched_priority(state); + if (err < 0) + return err; + } + + if (state->frames_per_period == 0) + state->frames_per_period = 512; + if (state->periods_per_buffer == 0) + state->periods_per_buffer = 2; + + return 0; +} + +static int r_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libffado_state *state = xfer->private_data; + unsigned int avail_count; + unsigned int bytes_per_frame; + unsigned int consumed_count; + int err; + + // Trim up to expected frame count. + avail_count = state->frames_per_period; + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&state->cache)) { + int ch; + int pos; + + // Register buffers. + pos = 0; + bytes_per_frame = state->cache.bytes_per_sample * + state->cache.samples_per_frame; + for (ch = 0; ch < state->data_ch_count; ++ch) { + char *buf; + + if (state->data_ch_map[ch] != ffado_stream_type_audio) + continue; + + buf = state->cache.buf_ptr; + buf += ch * bytes_per_frame; + if (ffado_streaming_set_capture_stream_buffer(state->handle, + ch, buf)) + return -EIO; + ++pos; + } + + assert(pos == xfer->samples_per_frame); + + // Move data to the buffer from intermediate buffer. + if (!ffado_streaming_transfer_buffers(state->handle)) + return -EIO; + + frame_cache_increase_count(&state->cache, + state->frames_per_period); + } + + // Write out to file descriptors. + consumed_count = frame_cache_get_count(&state->cache); + err = mapper_context_process_frames(mapper, state->cache.buf, + &consumed_count, cntrs); + if (err < 0) + return err; + + frame_cache_reduce(&state->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int w_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libffado_state *state = xfer->private_data; + unsigned int avail_count; + int pos; + int ch; + unsigned int bytes_per_frame; + unsigned int consumed_count; + int err; + + // Trim up to expected frame_count. + avail_count = state->frames_per_period; + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&state->cache)) { + avail_count -= frame_cache_get_count(&state->cache); + + err = mapper_context_process_frames(mapper, state->cache.buf_ptr, + &avail_count, cntrs); + if (err < 0) + return err; + frame_cache_increase_count(&state->cache, avail_count); + avail_count = state->cache.remained_count; + } + + // Register buffers. + pos = 0; + bytes_per_frame = state->cache.bytes_per_sample * + state->cache.samples_per_frame; + for (ch = 0; ch < state->data_ch_count; ++ch) { + char *buf; + + if (state->data_ch_map[ch] != ffado_stream_type_audio) + continue; + + buf = state->cache.buf; + buf += bytes_per_frame; + if (ffado_streaming_set_playback_stream_buffer(state->handle, + ch, buf)) + return -EIO; + ++pos; + } + + assert(pos == xfer->samples_per_frame); + + // Move data on the buffer for transmission. + if (!ffado_streaming_transfer_buffers(state->handle)) + return -EIO; + consumed_count = state->frames_per_period; + + frame_cache_reduce(&state->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int open_handle(struct xfer_context *xfer, + unsigned int frames_per_second) +{ + struct libffado_state *state = xfer->private_data; + ffado_options_t options = {0}; + ffado_device_info_t info = {0}; + + char str[32] = {0}; + char *strings[1]; + + // Set target unit if given. + if (state->port_literal != NULL) { + if (state->node_literal != NULL) { + snprintf(str, sizeof(str), "hw:%s,%s", + state->port_literal, state->node_literal); + } else { + snprintf(str, sizeof(str), "hw:%s", + state->port_literal); + } + } else if (state->guid_literal != NULL) { + snprintf(str, sizeof(str), "guid:%s", state->guid_literal); + } + if (str[0] != '\0') { + info.nb_device_spec_strings = 1; + strings[0] = str; + info.device_spec_strings = strings; + } + + // Set common options. + options.sample_rate = frames_per_second; + options.period_size = state->frames_per_period; + options.nb_buffers = state->periods_per_buffer; + options.realtime = !!(state->sched_priority > 0); + options.packetizer_priority = state->sched_priority; + options.slave_mode = state->slave_mode; + options.snoop_mode = state->snoop_mode; + options.verbose = xfer->verbose; + + state->handle = ffado_streaming_init(info, options); + if (state->handle == NULL) + return -EINVAL; + + return 0; +} + +static int enable_mbla_data_ch(struct libffado_state *state, + unsigned int *samples_per_frame) +{ + int (*func_type)(ffado_device_t *handle, int pos); + int (*func_onoff)(ffado_device_t *handle, int pos, int on); + int count; + int ch; + + if (state->direction == FFADO_CAPTURE) { + func_type = ffado_streaming_get_capture_stream_type; + func_onoff = ffado_streaming_capture_stream_onoff; + count = ffado_streaming_get_nb_capture_streams(state->handle); + } else { + func_type = ffado_streaming_get_playback_stream_type; + func_onoff = ffado_streaming_playback_stream_onoff; + count = ffado_streaming_get_nb_playback_streams(state->handle); + } + if (count <= 0) + return -EIO; + + state->data_ch_map = calloc(count, sizeof(*state->data_ch_map)); + if (state->data_ch_map == NULL) + return -ENOMEM; + state->data_ch_count = count; + + // When a data ch is off, data in the ch is truncated. This helps to + // align PCM frames in interleaved order. + *samples_per_frame = 0; + for (ch = 0; ch < count; ++ch) { + int on; + + state->data_ch_map[ch] = func_type(state->handle, ch); + + on = !!(state->data_ch_map[ch] == ffado_stream_type_audio); + if (func_onoff(state->handle, ch, on)) + return -EIO; + if (on) + ++(*samples_per_frame); + } + + return 0; +} + +static int xfer_libffado_pre_process(struct xfer_context *xfer, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer) +{ + struct libffado_state *state = xfer->private_data; + unsigned int channels; + int err; + + // Supported format of sample is 24 bit multi bit linear audio in + // AM824 format or the others. + if (state->direction == FFADO_CAPTURE) { + if (*format == SND_PCM_FORMAT_UNKNOWN) + *format = SND_PCM_FORMAT_S24; + } + if (*format != SND_PCM_FORMAT_S24) { + fprintf(stderr, + "A libffado backend supports S24 only.\n"); + return -EINVAL; + } + + // The backend requires the number of frames per second for its + // initialization. + if (state->direction == FFADO_CAPTURE) { + if (*frames_per_second == 0) + *frames_per_second = 48000; + } + if (*frames_per_second < 32000 || *frames_per_second > 192000) { + fprintf(stderr, + "A libffado backend supports sampling rate between " + "32000 and 192000, discretely.\n"); + return -EINVAL; + } + + err = open_handle(xfer, *frames_per_second); + if (err < 0) + return err; + + if (ffado_streaming_set_audio_datatype(state->handle, + ffado_audio_datatype_int24)) + return -EINVAL; + + // Decide buffer layout. + err = enable_mbla_data_ch(state, &channels); + if (err < 0) + return err; + + // This backend doesn't support resampling. + if (state->direction == FFADO_CAPTURE) { + if (*samples_per_frame == 0) + *samples_per_frame = channels; + } + if (*samples_per_frame != channels) { + fprintf(stderr, + "The number of samples per frame should be %u.\n", + channels); + return -EINVAL; + } + + // A buffer has interleaved-aligned PCM frames. + *access = SND_PCM_ACCESS_RW_INTERLEAVED; + *frames_per_buffer = + state->frames_per_period * state->periods_per_buffer; + + // Use cache for double number of frames per period. + err = frame_cache_init(&state->cache, *access, + snd_pcm_format_physical_width(*format) / 8, + *samples_per_frame, state->frames_per_period * 2); + if (err < 0) + return err; + + if (state->direction == FFADO_CAPTURE) + state->process_frames = r_process_frames; + else + state->process_frames = w_process_frames; + + if (ffado_streaming_prepare(state->handle)) + return -EIO; + + if (ffado_streaming_start(state->handle)) + return -EIO; + + return 0; +} + +static int xfer_libffado_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libffado_state *state = xfer->private_data; + ffado_wait_response res; + int err; + + res = ffado_streaming_wait(state->handle); + if (res == ffado_wait_shutdown || res == ffado_wait_error) { + err = -EIO; + } else if (res == ffado_wait_xrun) { + // No way to recover in this backend. + err = -EPIPE; + } else if (res == ffado_wait_ok) { + err = state->process_frames(xfer, frame_count, mapper, cntrs); + } else { + err = -ENXIO; + } + + if (err < 0) + *frame_count = 0; + + return err; +} + +static void xfer_libffado_pause(struct xfer_context *xfer, bool enable) +{ + struct libffado_state *state = xfer->private_data; + + // This is an emergency avoidance because this backend doesn't support + // suspend/aresume operation. + if (enable) { + ffado_streaming_stop(state->handle); + ffado_streaming_finish(state->handle); + exit(EXIT_FAILURE); + } +} + +static void xfer_libffado_post_process(struct xfer_context *xfer) +{ + struct libffado_state *state = xfer->private_data; + + if (state->handle != NULL) { + ffado_streaming_stop(state->handle); + ffado_streaming_finish(state->handle); + } + + frame_cache_destroy(&state->cache); + free(state->data_ch_map); + state->data_ch_map = NULL; +} + +static void xfer_libffado_destroy(struct xfer_context *xfer) +{ + struct libffado_state *state = xfer->private_data; + + free(state->port_literal); + free(state->node_literal); + free(state->guid_literal); + state->port_literal = NULL; + state->node_literal = NULL; + state->guid_literal = NULL; +} + +static void xfer_libffado_help(struct xfer_context *xfer) +{ + printf( +" -p, --port decimal ID of port to decide 1394 OHCI controller for communication on IEEE 1394 bus\n" +" -n, --node decimal ID of node to decide unit on IEEE 1394 bus for transmission of audio data frame\n" +" -g, --guid hexadecimal ID for node on IEEE 1394 bus for transmission of audio data frame\n" +" --frames-per-period the number of audio data frame to handle one operation (frame unit)\n" +" --periods-per-bufer the number of periods in intermediate buffer between libffado (frame unit)\n" +" --slave receive frames from the other Linux system on IEEE 1394 bus running with libffado.\n" +" --snoop receive frames on packets of all isochronous channels.\n" +" --sched-priority set SCHED_FIFO with given priority. see RLIMIT_RTPRIO in getrlimit(2).\n" + ); +} + +const struct xfer_data xfer_libffado = { + .s_opts = S_OPTS, + .l_opts = l_opts, + .l_opts_count = ARRAY_SIZE(l_opts), + .ops = { + .init = xfer_libffado_init, + .parse_opt = xfer_libffado_parse_opt, + .validate_opts = xfer_libffado_validate_opts, + .pre_process = xfer_libffado_pre_process, + .process_frames = xfer_libffado_process_frames, + .pause = xfer_libffado_pause, + .post_process = xfer_libffado_post_process, + .destroy = xfer_libffado_destroy, + .help = xfer_libffado_help, + }, + .private_size = sizeof(struct libffado_state), +}; diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c new file mode 100644 index 0000000..e716ded --- /dev/null +++ b/axfer/xfer-options.c @@ -0,0 +1,628 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-options.c - a parser of commandline options for xfer. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer.h" +#include "misc.h" + +#include +#include +#include + +enum no_short_opts { + // 128 or later belong to non us-ascii character set. + OPT_XFER_TYPE = 128, + OPT_DUMP_HW_PARAMS, + OPT_PERIOD_SIZE, + OPT_BUFFER_SIZE, + // Obsoleted. + OPT_MAX_FILE_TIME, + OPT_USE_STRFTIME, + OPT_PROCESS_ID_FILE, +}; + +static void print_help() +{ + printf( +"Usage:\n" +" axfer transfer DIRECTION [ COMMON-OPTIONS ] [ BACKEND-OPTIONS ]\n" +"\n" +" where:\n" +" DIRECTION = capture | playback\n" +" COMMON-OPTIONS =\n" +" -h, --help help\n" +" -v, --verbose verbose\n" +" -q, --quiet quiet mode\n" +" -d, --duration=# interrupt after # seconds\n" +" -s, --samples=# interrupt after # frames\n" +" -f, --format=FORMAT sample format (case-insensitive)\n" +" -c, --channels=# channels\n" +" -r, --rate=# numeric sample rate in unit of Hz or kHz\n" +" -t, --file-type=TYPE file type (wav, au, sparc, voc or raw, case-insentive)\n" +" -I, --separate-channels one file for each channel\n" +" --dump-hw-params dump hw_params of the device\n" +" --xfer-type=BACKEND backend type (libasound, libffado)\n" + ); +} + +static int allocate_paths(struct xfer_context *xfer, char *const *paths, + unsigned int count) +{ + bool stdio = false; + unsigned int i; + + if (count == 0) { + stdio = true; + count = 1; + } + + xfer->paths = calloc(count, sizeof(xfer->paths[0])); + if (xfer->paths == NULL) + return -ENOMEM; + xfer->path_count = count; + + if (stdio) { + xfer->paths[0] = strdup("-"); + if (xfer->paths[0] == NULL) + return -ENOMEM; + } else { + for (i = 0; i < count; ++i) { + xfer->paths[i] = strndup(paths[i], PATH_MAX); + if (xfer->paths[i] == NULL) + return -ENOMEM; + } + } + + return 0; +} + +static int verify_cntr_format(struct xfer_context *xfer) +{ + static const struct { + const char *const literal; + enum container_format cntr_format; + } *entry, entries[] = { + {"raw", CONTAINER_FORMAT_RAW}, + {"voc", CONTAINER_FORMAT_VOC}, + {"wav", CONTAINER_FORMAT_RIFF_WAVE}, + {"au", CONTAINER_FORMAT_AU}, + {"sparc", CONTAINER_FORMAT_AU}, + }; + int i; + + for (i = 0; i < (int)ARRAY_SIZE(entries); ++i) { + entry = &entries[i]; + if (strcasecmp(xfer->cntr_format_literal, entry->literal)) + continue; + + xfer->cntr_format = entry->cntr_format; + return 0; + } + + fprintf(stderr, "unrecognized file format '%s'\n", + xfer->cntr_format_literal); + + return -EINVAL; +} + +// This should be called after 'verify_cntr_format()'. +static int verify_sample_format(struct xfer_context *xfer) +{ + static const struct { + const char *const literal; + unsigned int frames_per_second; + unsigned int samples_per_frame; + snd_pcm_format_t le_format; + snd_pcm_format_t be_format; + } *entry, entries[] = { + {"cd", 44100, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, + {"cdr", 44100, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, + {"dat", 48000, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, + }; + unsigned int i; + + xfer->sample_format = snd_pcm_format_value(xfer->sample_format_literal); + if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) + return 0; + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + entry = &entries[i]; + if (strcmp(entry->literal, xfer->sample_format_literal)) + continue; + + if (xfer->frames_per_second > 0 && + xfer->frames_per_second != entry->frames_per_second) { + fprintf(stderr, + "'%s' format can't be used with rate except " + "for %u.\n", + entry->literal, entry->frames_per_second); + return -EINVAL; + } + + if (xfer->samples_per_frame > 0 && + xfer->samples_per_frame != entry->samples_per_frame) { + fprintf(stderr, + "'%s' format can't be used with channel except " + "for %u.\n", + entry->literal, entry->samples_per_frame); + return -EINVAL; + } + + xfer->frames_per_second = entry->frames_per_second; + xfer->samples_per_frame = entry->samples_per_frame; + if (xfer->cntr_format == CONTAINER_FORMAT_AU) + xfer->sample_format = entry->be_format; + else + xfer->sample_format = entry->le_format; + + return 0; + } + + fprintf(stderr, "wrong extended format '%s'\n", + xfer->sample_format_literal); + + return -EINVAL; +} + +static int validate_options(struct xfer_context *xfer) +{ + unsigned int val; + int err = 0; + + if (xfer->cntr_format_literal == NULL) { + if (xfer->direction == SND_PCM_STREAM_CAPTURE) { + // To stdout. + if (xfer->path_count == 1 && + !strcmp(xfer->paths[0], "-")) { + xfer->cntr_format = CONTAINER_FORMAT_RAW; + } else { + // Use first path as a representative. + xfer->cntr_format = container_format_from_path( + xfer->paths[0]); + } + } + // For playback, perform auto-detection. + } else { + err = verify_cntr_format(xfer); + } + if (err < 0) + return err; + + if (xfer->multiple_cntrs) { + if (!strcmp(xfer->paths[0], "-")) { + fprintf(stderr, + "An option for separated channels is not " + "available with stdin/stdout.\n"); + return -EINVAL; + } + + // For captured PCM frames, even if one path is given for + // container files, it can be used to generate several paths. + // For this purpose, please see + // 'xfer_options_fixup_paths()'. + if (xfer->direction == SND_PCM_STREAM_PLAYBACK) { + // Require several paths for containers. + if (xfer->path_count == 1) { + fprintf(stderr, + "An option for separated channels " + "requires several files to playback " + "PCM frames.\n"); + return -EINVAL; + } + } + } else { + // A single path is available only. + if (xfer->path_count > 1) { + fprintf(stderr, + "When using several files, an option for " + "sepatated channels is used with.\n"); + return -EINVAL; + } + } + + xfer->sample_format = SND_PCM_FORMAT_UNKNOWN; + if (xfer->sample_format_literal) { + err = verify_sample_format(xfer); + if (err < 0) + return err; + } + + val = xfer->frames_per_second; + if (xfer->frames_per_second == 0) + xfer->frames_per_second = 8000; + if (xfer->frames_per_second < 1000) + xfer->frames_per_second *= 1000; + if (xfer->frames_per_second < 2000 || + xfer->frames_per_second > 192000) { + fprintf(stderr, "bad speed value '%u'\n", val); + return -EINVAL; + } + + if (xfer->samples_per_frame > 0) { + if (xfer->samples_per_frame < 1 || + xfer->samples_per_frame > 256) { + fprintf(stderr, "invalid channels argument '%u'\n", + xfer->samples_per_frame); + return -EINVAL; + } + } + + return err; +} + +int xfer_options_parse_args(struct xfer_context *xfer, + const struct xfer_data *data, int argc, + char *const *argv) +{ + static const char *short_opts = "CPhvqd:s:f:c:r:t:IV:i"; + static const struct option long_opts[] = { + // For generic purposes. + {"capture", 0, 0, 'C'}, + {"playback", 0, 0, 'P'}, + {"xfer-type", 1, 0, OPT_XFER_TYPE}, + {"help", 0, 0, 'h'}, + {"verbose", 0, 0, 'v'}, + {"quiet", 0, 0, 'q'}, + {"duration", 1, 0, 'd'}, + {"samples", 1, 0, 's'}, + // For transfer backend. + {"format", 1, 0, 'f'}, + {"channels", 1, 0, 'c'}, + {"rate", 1, 0, 'r'}, + // For containers. + {"file-type", 1, 0, 't'}, + // For mapper. + {"separate-channels", 0, 0, 'I'}, + // For debugging. + {"dump-hw-params", 0, 0, OPT_DUMP_HW_PARAMS}, + // Obsoleted. + {"max-file-time", 1, 0, OPT_MAX_FILE_TIME}, + {"use-strftime", 0, 0, OPT_USE_STRFTIME}, + {"process-id-file", 1, 0, OPT_PROCESS_ID_FILE}, + {"vumeter", 1, 0, 'V'}, + {"interactive", 0, 0, 'i'}, + }; + char *s_opts; + struct option *l_opts; + int l_index; + int key; + int err = 0; + + // Concatenate short options. + s_opts = malloc(strlen(data->s_opts) + strlen(short_opts) + 1); + if (s_opts == NULL) + return -ENOMEM; + strcpy(s_opts, data->s_opts); + strcpy(s_opts + strlen(s_opts), short_opts); + s_opts[strlen(data->s_opts) + strlen(short_opts)] = '\0'; + + // Concatenate long options, including a sentinel. + l_opts = calloc(ARRAY_SIZE(long_opts) * data->l_opts_count + 1, + sizeof(*l_opts)); + if (l_opts == NULL) { + free(s_opts); + return -ENOMEM; + } + memcpy(l_opts, long_opts, ARRAY_SIZE(long_opts) * sizeof(*l_opts)); + memcpy(&l_opts[ARRAY_SIZE(long_opts)], data->l_opts, + data->l_opts_count * sizeof(*l_opts)); + + // Parse options. + l_index = 0; + optarg = NULL; + optind = 1; + opterr = 1; // use error output. + optopt = 0; + while (1) { + key = getopt_long(argc, argv, s_opts, l_opts, &l_index); + if (key < 0) + break; + else if (key == 'C') + ; // already parsed. + else if (key == 'P') + ; // already parsed. + else if (key == OPT_XFER_TYPE) + ; // already parsed. + else if (key == 'h') + xfer->help = true; + else if (key == 'v') + ++xfer->verbose; + else if (key == 'q') + xfer->quiet = true; + else if (key == 'd') + xfer->duration_seconds = arg_parse_decimal_num(optarg, &err); + else if (key == 's') + xfer->duration_frames = arg_parse_decimal_num(optarg, &err); + else if (key == 'f') + xfer->sample_format_literal = arg_duplicate_string(optarg, &err); + else if (key == 'c') + xfer->samples_per_frame = arg_parse_decimal_num(optarg, &err); + else if (key == 'r') + xfer->frames_per_second = arg_parse_decimal_num(optarg, &err); + else if (key == 't') + xfer->cntr_format_literal = arg_duplicate_string(optarg, &err); + else if (key == 'I') + xfer->multiple_cntrs = true; + else if (key == OPT_DUMP_HW_PARAMS) + xfer->dump_hw_params = true; + else if (key == '?') { + free(l_opts); + free(s_opts); + return -EINVAL; + } + else if (key == OPT_MAX_FILE_TIME || + key == OPT_USE_STRFTIME || + key == OPT_PROCESS_ID_FILE || + key == 'V' || + key == 'i') { + fprintf(stderr, + "An option '--%s' is obsoleted and has no " + "effect.\n", + l_opts[l_index].name); + err = -EINVAL; + } else { + err = xfer->ops->parse_opt(xfer, key, optarg); + if (err < 0 && err != -ENXIO) + break; + } + } + + free(l_opts); + free(s_opts); + + if (xfer->help) { + print_help(); + if (xfer->ops->help) { + printf("\n"); + printf(" BACKEND-OPTIONS (%s) =\n", + xfer_label_from_type(xfer->type)); + xfer->ops->help(xfer); + } + return 0; + } + + err = allocate_paths(xfer, argv + optind, argc - optind); + if (err < 0) + return err; + + return validate_options(xfer); +} + +void xfer_options_calculate_duration(struct xfer_context *xfer, + uint64_t *total_frame_count) +{ + uint64_t frame_count; + + if (xfer->duration_seconds > 0) { + frame_count = (uint64_t)xfer->duration_seconds * (uint64_t)xfer->frames_per_second; + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } + + if (xfer->duration_frames > 0) { + frame_count = xfer->duration_frames; + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } +} + +static const char *const allowed_duplication[] = { + "/dev/null", + "/dev/zero", + "/dev/full", + "/dev/random", + "/dev/urandom", +}; + +static int generate_path_with_suffix(struct xfer_context *xfer, + const char *template, unsigned int index, + const char *suffix) +{ + static const char *const single_format = "%s%s"; + static const char *const multiple_format = "%s-%i%s"; + unsigned int len; + + len = strlen(template) + strlen(suffix) + 1; + if (xfer->path_count > 1) + len += (unsigned int)log10(xfer->path_count) + 2; + + xfer->paths[index] = malloc(len); + if (xfer->paths[index] == NULL) + return -ENOMEM; + + if (xfer->path_count == 1) { + snprintf(xfer->paths[index], len, single_format, template, + suffix); + } else { + snprintf(xfer->paths[index], len, multiple_format, template, + index, suffix); + } + + return 0; +} + +static int generate_path_without_suffix(struct xfer_context *xfer, + const char *template, + unsigned int index, + const char *suffix ATTRIBUTE_UNUSED) +{ + static const char *const single_format = "%s"; + static const char *const multiple_format = "%s-%i"; + unsigned int len; + + len = strlen(template) + 1; + if (xfer->path_count > 1) + len += (unsigned int)log10(xfer->path_count) + 2; + + xfer->paths[index] = malloc(len); + if (xfer->paths[index] == NULL) + return -ENOMEM; + + if (xfer->path_count == 1) { + snprintf(xfer->paths[index], len, single_format, template); + } else { + snprintf(xfer->paths[index], len, multiple_format, template, + index); + } + + return 0; +} + +static int generate_path(struct xfer_context *xfer, char *template, + unsigned int index, const char *suffix) +{ + int (*generator)(struct xfer_context *xfer, const char *template, + unsigned int index, const char *suffix); + char *pos; + + if (strlen(suffix) > 0) { + pos = template + strlen(template) - strlen(suffix); + // Separate filename and suffix. + if (!strcmp(pos, suffix)) + *pos = '\0'; + } + + // Select handlers. + if (strlen(suffix) > 0) + generator = generate_path_with_suffix; + else + generator = generate_path_without_suffix; + + return generator(xfer, template, index, suffix); +} + +static int create_paths(struct xfer_context *xfer, unsigned int path_count) +{ + char *template; + const char *suffix; + unsigned int i, j; + int err = 0; + + // Can cause memory leak. + assert(xfer->path_count == 1); + assert(xfer->paths); + assert(xfer->paths[0]); + assert(xfer->paths[0][0] != '\0'); + + // Release at first. + template = xfer->paths[0]; + free(xfer->paths); + xfer->paths = NULL; + + // Allocate again. + xfer->paths = calloc(path_count, sizeof(*xfer->paths)); + if (xfer->paths == NULL) { + err = -ENOMEM; + goto end; + } + xfer->path_count = path_count; + + suffix = container_suffix_from_format(xfer->cntr_format); + + for (i = 0; i < xfer->path_count; ++i) { + // Some file names are allowed to be duplicated. + for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { + if (!strcmp(template, allowed_duplication[j])) + break; + } + if (j < ARRAY_SIZE(allowed_duplication)) + continue; + + err = generate_path(xfer, template, i, suffix); + if (err < 0) + break; + } +end: + free(template); + + return err; +} + +static int fixup_paths(struct xfer_context *xfer) +{ + const char *suffix; + char *template; + unsigned int i, j; + int err = 0; + + suffix = container_suffix_from_format(xfer->cntr_format); + + for (i = 0; i < xfer->path_count; ++i) { + // Some file names are allowed to be duplicated. + for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { + if (!strcmp(xfer->paths[i], allowed_duplication[j])) + break; + } + if (j < ARRAY_SIZE(allowed_duplication)) + continue; + + template = xfer->paths[i]; + xfer->paths[i] = NULL; + err = generate_path(xfer, template, i, suffix); + free(template); + if (err < 0) + break; + } + + return err; +} + +int xfer_options_fixup_paths(struct xfer_context *xfer) +{ + unsigned int i, j; + int err; + + if (xfer->path_count == 1) { + // Nothing to do for sign of stdin/stdout. + if (!strcmp(xfer->paths[0], "-")) + return 0; + if (!xfer->multiple_cntrs) + err = fixup_paths(xfer); + else + err = create_paths(xfer, xfer->samples_per_frame); + } else { + if (!xfer->multiple_cntrs) + return -EINVAL; + if (xfer->path_count != xfer->samples_per_frame) + return -EINVAL; + else + err = fixup_paths(xfer); + } + if (err < 0) + return err; + + // Check duplication of the paths. + for (i = 0; i < xfer->path_count - 1; ++i) { + // Some file names are allowed to be duplicated. + for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { + if (!strcmp(xfer->paths[i], allowed_duplication[j])) + break; + } + if (j < ARRAY_SIZE(allowed_duplication)) + continue; + + for (j = i + 1; j < xfer->path_count; ++j) { + if (!strcmp(xfer->paths[i], xfer->paths[j])) { + fprintf(stderr, + "Detect duplicated file names:\n"); + err = -EINVAL; + break; + } + } + if (j < xfer->path_count) + break; + } + + if (xfer->verbose > 1) + fprintf(stderr, "Handled file names:\n"); + if (err < 0 || xfer->verbose > 1) { + for (i = 0; i < xfer->path_count; ++i) + fprintf(stderr, " %d: %s\n", i, xfer->paths[i]); + } + + return err; +} diff --git a/axfer/xfer.c b/axfer/xfer.c new file mode 100644 index 0000000..3750e8a --- /dev/null +++ b/axfer/xfer.c @@ -0,0 +1,259 @@ + +// +// xfer.c - receiver/transmiter of data frames. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer.h" +#include "misc.h" + +#include + +static const char *const xfer_type_labels[] = { + [XFER_TYPE_LIBASOUND] = "libasound", +#if WITH_FFADO + [XFER_TYPE_LIBFFADO] = "libffado", +#endif +}; + +enum xfer_type xfer_type_from_label(const char *label) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(xfer_type_labels); ++i) { + if (!strcmp(xfer_type_labels[i], label)) + return i; + } + + return XFER_TYPE_UNSUPPORTED; +} + +const char *xfer_label_from_type(enum xfer_type type) +{ + return xfer_type_labels[type]; +} + +int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, + snd_pcm_stream_t direction, int argc, char *const *argv) +{ + struct { + enum xfer_type type; + const struct xfer_data *data; + } *entry, entries[] = { + {XFER_TYPE_LIBASOUND, &xfer_libasound}, +#if WITH_FFADO + {XFER_TYPE_LIBFFADO, &xfer_libffado}, +#endif + }; + unsigned int i; + int err; + + assert(xfer); + assert(direction >= SND_PCM_STREAM_PLAYBACK); + assert(direction <= SND_PCM_STREAM_CAPTURE); + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + if (entries[i].type == type) + break; + } + if (i == ARRAY_SIZE(entries)) + return -EINVAL; + entry = &entries[i]; + + xfer->direction = direction; + xfer->type = type; + xfer->ops = &entry->data->ops; + + xfer->private_data = malloc(entry->data->private_size); + if (xfer->private_data == NULL) + return -ENOMEM; + memset(xfer->private_data, 0, entry->data->private_size); + + err = xfer->ops->init(xfer, direction); + if (err < 0) + return err; + + err = xfer_options_parse_args(xfer, entry->data, argc, argv); + if (err < 0) + return err; + + return xfer->ops->validate_opts(xfer); +} + +void xfer_context_destroy(struct xfer_context *xfer) +{ + unsigned int i; + + assert(xfer); + + if (!xfer->ops) + return; + + if (xfer->ops->destroy) + xfer->ops->destroy(xfer); + if (xfer->private_data) + free(xfer->private_data); + + if (xfer->paths) { + for (i = 0; i < xfer->path_count; ++i) + free(xfer->paths[i]); + free(xfer->paths); + } + + xfer->paths = NULL; + + free(xfer->sample_format_literal); + xfer->sample_format_literal = NULL; + + free(xfer->cntr_format_literal); + xfer->cntr_format_literal = NULL; +} + +int xfer_context_pre_process(struct xfer_context *xfer, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer) +{ + int err; + + assert(xfer); + assert(format); + assert(samples_per_frame); + assert(frames_per_second); + assert(access); + assert(frames_per_buffer); + + if (!xfer->ops) + return -ENXIO; + + if (xfer->direction == SND_PCM_STREAM_CAPTURE) { + // For capture direction, use values in options if given. + if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) + *format = xfer->sample_format; + if (xfer->samples_per_frame > 0) + *samples_per_frame = xfer->samples_per_frame; + if (xfer->frames_per_second > 0) + *frames_per_second = xfer->frames_per_second; + } else if (xfer->direction == SND_PCM_STREAM_PLAYBACK) { + // For playback direction, check values in given options so that + // they don't mismatch to parameters from media container. + if (*format != xfer->sample_format) { + // Not initial value. + if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) { + fprintf(stderr, + "Sample format mismatch: %s is given " + "but %s by files\n", + snd_pcm_format_name(xfer->sample_format), + snd_pcm_format_name(*format)); + return -EINVAL; + } + } + + if (*samples_per_frame != xfer->samples_per_frame) { + // Not initial value. + if (xfer->samples_per_frame > 0) { + fprintf(stderr, + "The number of channels mismatch: %u " + "is given but %u by files\n", + xfer->samples_per_frame, + *samples_per_frame); + return -EINVAL; + } + } + + if (*frames_per_second != xfer->frames_per_second) { + // Not initial value. + if (xfer->frames_per_second != 8000) { + fprintf(stderr, + "Sampling rate mismatch: %u is given " + "but %u by files\n", + xfer->frames_per_second, + *frames_per_second); + return -EINVAL; + } + } + } + + err = xfer->ops->pre_process(xfer, format, samples_per_frame, + frames_per_second, access, + frames_per_buffer); + if (err < 0) + return err; + + assert(*format >= SND_PCM_FORMAT_S8); + assert(*format <= SND_PCM_FORMAT_LAST); + assert(*samples_per_frame > 0); + assert(*frames_per_second > 0); + assert(*access >= SND_PCM_ACCESS_MMAP_INTERLEAVED); + assert(*access <= SND_PCM_ACCESS_LAST); + assert(*frames_per_buffer > 0); + + xfer->sample_format = *format; + xfer->samples_per_frame = *samples_per_frame; + xfer->frames_per_second = *frames_per_second; + + if (xfer->direction == SND_PCM_STREAM_CAPTURE) { + err = xfer_options_fixup_paths(xfer); + if (err < 0) + return err; + } + + if (xfer->verbose > 1) { + fprintf(stderr, "Transfer: %s\n", + xfer_type_labels[xfer->type]); + fprintf(stderr, " access: %s\n", + snd_pcm_access_name(*access)); + fprintf(stderr, " sample format: %s\n", + snd_pcm_format_name(*format)); + fprintf(stderr, " bytes/sample: %u\n", + snd_pcm_format_physical_width(*format) / 8); + fprintf(stderr, " samples/frame: %u\n", + *samples_per_frame); + fprintf(stderr, " frames/second: %u\n", + *frames_per_second); + fprintf(stderr, " frames/buffer: %lu\n", + *frames_per_buffer); + } + + return 0; +} + +int xfer_context_process_frames(struct xfer_context *xfer, + struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int *frame_count) +{ + assert(xfer); + assert(mapper); + assert(cntrs); + assert(frame_count); + + if (!xfer->ops) + return -ENXIO; + + return xfer->ops->process_frames(xfer, frame_count, mapper, cntrs); +} + +void xfer_context_pause(struct xfer_context *xfer, bool enable) +{ + assert(xfer); + + if (!xfer->ops) + return; + + xfer->ops->pause(xfer, enable); +} + +void xfer_context_post_process(struct xfer_context *xfer) +{ + assert(xfer); + + if (!xfer->ops) + return; + + xfer->ops->post_process(xfer); +} diff --git a/axfer/xfer.h b/axfer/xfer.h new file mode 100644 index 0000000..b5a0e29 --- /dev/null +++ b/axfer/xfer.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer.h - a header for receiver/transmiter of data frames. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_XFER__H_ +#define __ALSA_UTILS_AXFER_XFER__H_ + +#include "aconfig.h" + +#include "mapper.h" + +#include + +enum xfer_type { + XFER_TYPE_UNSUPPORTED = -1, + XFER_TYPE_LIBASOUND = 0, +#if WITH_FFADO + XFER_TYPE_LIBFFADO, +#endif + XFER_TYPE_COUNT, +}; + +struct xfer_ops; + +struct xfer_context { + snd_pcm_stream_t direction; + enum xfer_type type; + const struct xfer_ops *ops; + void *private_data; + + char *sample_format_literal; + char *cntr_format_literal; + unsigned int verbose; + unsigned int duration_seconds; + unsigned int duration_frames; + unsigned int frames_per_second; + unsigned int samples_per_frame; + bool help:1; + bool quiet:1; + bool dump_hw_params:1; + bool multiple_cntrs:1; // For mapper. + + snd_pcm_format_t sample_format; + + // For containers. + char **paths; + unsigned int path_count; + enum container_format cntr_format; +}; + +enum xfer_type xfer_type_from_label(const char *label); +const char *xfer_label_from_type(enum xfer_type type); + +int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, + snd_pcm_stream_t direction, int argc, char *const *argv); +void xfer_context_destroy(struct xfer_context *xfer); +int xfer_context_pre_process(struct xfer_context *xfer, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer); +int xfer_context_process_frames(struct xfer_context *xfer, + struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int *frame_count); +void xfer_context_pause(struct xfer_context *xfer, bool enable); +void xfer_context_post_process(struct xfer_context *xfer); + +struct xfer_data; +int xfer_options_parse_args(struct xfer_context *xfer, + const struct xfer_data *data, int argc, + char *const *argv); +int xfer_options_fixup_paths(struct xfer_context *xfer); +void xfer_options_calculate_duration(struct xfer_context *xfer, + uint64_t *total_frame_count); + +// For internal use in 'xfer' module. + +struct xfer_ops { + int (*init)(struct xfer_context *xfer, snd_pcm_stream_t direction); + int (*parse_opt)(struct xfer_context *xfer, int key, const char *optarg); + int (*validate_opts)(struct xfer_context *xfer); + int (*pre_process)(struct xfer_context *xfer, snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer); + int (*process_frames)(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + void (*post_process)(struct xfer_context *xfer); + void (*destroy)(struct xfer_context *xfer); + void (*pause)(struct xfer_context *xfer, bool enable); + void (*help)(struct xfer_context *xfer); +}; + +struct xfer_data { + const char *s_opts; + const struct option *l_opts; + unsigned int l_opts_count; + struct xfer_ops ops; + unsigned int private_size; +}; + +extern const struct xfer_data xfer_libasound; + +#if WITH_FFADO + extern const struct xfer_data xfer_libffado; +#endif + +#endif diff --git a/bat/Makefile.am b/bat/Makefile.am new file mode 100644 index 0000000..5151f77 --- /dev/null +++ b/bat/Makefile.am @@ -0,0 +1,36 @@ +SUBDIRS=tests +bin_PROGRAMS = alsabat +man_MANS = alsabat.1 +EXTRA_DIST = alsabat.1 alsabat-test.sh +sbin_SCRIPTS = alsabat-test.sh + +alsabat_SOURCES = \ + bat.c \ + common.c \ + signal.c \ + latencytest.c \ + convert.c + +noinst_HEADERS = \ + common.h \ + bat-signal.h \ + latencytest.h \ + convert.h + +if HAVE_LIBFFTW3 +alsabat_SOURCES += analyze.c +noinst_HEADERS += analyze.h +endif + +if HAVE_LIBTINYALSA +alsabat_SOURCES += tinyalsa.c +noinst_HEADERS += tinyalsa.h +else +alsabat_SOURCES += alsa.c +noinst_HEADERS += alsa.h +endif + +AM_CPPFLAGS = \ + -Wall -I$(top_srcdir)/include + +alsabat_LDADD = @FFTW_LIB@ diff --git a/bat/alsa.c b/bat/alsa.c new file mode 100644 index 0000000..67489c4 --- /dev/null +++ b/bat/alsa.c @@ -0,0 +1,733 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "gettext.h" + +#include "common.h" +#include "alsa.h" +#include "latencytest.h" +#include "os_compat.h" + +struct pcm_container { + snd_pcm_t *handle; + snd_pcm_uframes_t period_size; + snd_pcm_uframes_t buffer_size; + snd_pcm_format_t format; + unsigned short channels; + size_t period_bytes; + size_t sample_bits; + size_t frame_bits; + char *buffer; +}; + +struct format_map_table { + enum _bat_pcm_format format_bat; + snd_pcm_format_t format_alsa; +}; + +static struct format_map_table map_tables[] = { + { BAT_PCM_FORMAT_UNKNOWN, SND_PCM_FORMAT_UNKNOWN }, + { BAT_PCM_FORMAT_U8, SND_PCM_FORMAT_U8 }, + { BAT_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_LE }, + { BAT_PCM_FORMAT_S24_3LE, SND_PCM_FORMAT_S24_3LE }, + { BAT_PCM_FORMAT_S32_LE, SND_PCM_FORMAT_S32_LE }, + { BAT_PCM_FORMAT_MAX, 0 }, +}; + +static int format_convert(struct bat *bat, snd_pcm_format_t *fmt) +{ + struct format_map_table *t = map_tables; + + for (; t->format_bat != BAT_PCM_FORMAT_MAX; t++) { + if (t->format_bat == bat->format) { + *fmt = t->format_alsa; + return 0; + } + } + fprintf(bat->err, _("Invalid format!\n")); + return -EINVAL; +} + +static int set_snd_pcm_params(struct bat *bat, struct pcm_container *sndpcm) +{ + snd_pcm_hw_params_t *params; + snd_pcm_format_t format; + unsigned int buffer_time = 0; + unsigned int period_time = 0; + snd_pcm_uframes_t buffer_size = 0; + snd_pcm_uframes_t period_size = 0; + unsigned int rate; + int err; + const char *device_name = snd_pcm_name(sndpcm->handle); + + /* Convert common format to ALSA format */ + err = format_convert(bat, &format); + if (err != 0) + return err; + + /* Allocate a hardware parameters object. */ + snd_pcm_hw_params_alloca(¶ms); + + /* Fill it in with default values. */ + err = snd_pcm_hw_params_any(sndpcm->handle, params); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("default params: %s: %s(%d)\n"), + device_name, snd_strerror(err), err); + return err; + } + + /* Set access mode */ + err = snd_pcm_hw_params_set_access(sndpcm->handle, params, + SND_PCM_ACCESS_RW_INTERLEAVED); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("access type: %s: %s(%d)\n"), + device_name, snd_strerror(err), err); + return err; + } + + /* Set format */ + err = snd_pcm_hw_params_set_format(sndpcm->handle, params, format); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("PCM format: %d %s: %s(%d)\n"), format, + device_name, snd_strerror(err), err); + return err; + } + + /* Set channels */ + err = snd_pcm_hw_params_set_channels(sndpcm->handle, + params, bat->channels); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("channel number: %d %s: %s(%d)\n"), + bat->channels, + device_name, snd_strerror(err), err); + return err; + } + + /* Set sampling rate */ + rate = bat->rate; + err = snd_pcm_hw_params_set_rate_near(sndpcm->handle, + params, &bat->rate, + 0); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("sample rate: %d %s: %s(%d)\n"), + bat->rate, + device_name, snd_strerror(err), err); + return err; + } + if ((float) rate * (1 + RATE_RANGE) < bat->rate + || (float) rate * (1 - RATE_RANGE) > bat->rate) { + fprintf(bat->err, _("Invalid parameters: sample rate: ")); + fprintf(bat->err, _("requested %dHz, got %dHz\n"), + rate, bat->rate); + return -EINVAL; + } + + if (bat->buffer_size > 0 && bat->period_size == 0) + bat->period_size = bat->buffer_size / DIV_BUFFERSIZE; + + if (bat->roundtriplatency && bat->buffer_size == 0) { + /* Set to minimum buffer size and period size + for latency test */ + if (snd_pcm_hw_params_get_buffer_size_min(params, + &buffer_size) < 0) { + fprintf(bat->err, + _("Get parameter from device error: ")); + fprintf(bat->err, _("buffer size min: %d %s: %s(%d)\n"), + (int) buffer_size, + device_name, snd_strerror(err), err); + return -EINVAL; + } + + if (snd_pcm_hw_params_get_period_size_min(params, + &period_size, 0) < 0) { + fprintf(bat->err, + _("Get parameter from device error: ")); + fprintf(bat->err, _("period size min: %d %s: %s(%d)\n"), + (int) period_size, + device_name, snd_strerror(err), err); + return -EINVAL; + } + bat->buffer_size = (int) buffer_size; + bat->period_size = (int) period_size; + } + + if (bat->buffer_size > 0) { + buffer_size = bat->buffer_size; + period_size = bat->period_size; + + fprintf(bat->log, _("Set period size: %d buffer size: %d\n"), + (int) period_size, (int) buffer_size); + + err = snd_pcm_hw_params_set_buffer_size_near(sndpcm->handle, + params, &buffer_size); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("buffer size: %d %s: %s(%d)\n"), + (int) buffer_size, + device_name, snd_strerror(err), err); + return err; + } + + err = snd_pcm_hw_params_set_period_size_near(sndpcm->handle, + params, &period_size, 0); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("period size: %d %s: %s(%d)\n"), + (int) period_size, + device_name, snd_strerror(err), err); + return err; + } + } else { + if (snd_pcm_hw_params_get_buffer_time_max(params, + &buffer_time, 0) < 0) { + fprintf(bat->err, + _("Get parameter from device error: ")); + fprintf(bat->err, _("buffer time: %d %s: %s(%d)\n"), + buffer_time, + device_name, snd_strerror(err), err); + return -EINVAL; + } + + if (buffer_time > MAX_BUFFERTIME) + buffer_time = MAX_BUFFERTIME; + + period_time = buffer_time / DIV_BUFFERTIME; + + /* Set buffer time and period time */ + err = snd_pcm_hw_params_set_buffer_time_near(sndpcm->handle, + params, &buffer_time, 0); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("buffer time: %d %s: %s(%d)\n"), + buffer_time, + device_name, snd_strerror(err), err); + return err; + } + + err = snd_pcm_hw_params_set_period_time_near(sndpcm->handle, + params, &period_time, 0); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("period time: %d %s: %s(%d)\n"), + period_time, + device_name, snd_strerror(err), err); + return err; + } + } + + /* Write the parameters to the driver */ + if (snd_pcm_hw_params(sndpcm->handle, params) < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("hw params: %s: %s(%d)\n"), + device_name, snd_strerror(err), err); + return -EINVAL; + } + + err = snd_pcm_hw_params_get_period_size(params, + &sndpcm->period_size, 0); + if (err < 0) { + fprintf(bat->err, _("Get parameter from device error: ")); + fprintf(bat->err, _("period size: %zd %s: %s(%d)\n"), + sndpcm->period_size, + device_name, snd_strerror(err), err); + return err; + } + + err = snd_pcm_hw_params_get_buffer_size(params, &sndpcm->buffer_size); + if (err < 0) { + fprintf(bat->err, _("Get parameter from device error: ")); + fprintf(bat->err, _("buffer size: %zd %s: %s(%d)\n"), + sndpcm->buffer_size, + device_name, snd_strerror(err), err); + return err; + } + + if (sndpcm->period_size == sndpcm->buffer_size) { + fprintf(bat->err, _("Invalid parameters: can't use period ")); + fprintf(bat->err, _("equal to buffer size (%zd)\n"), + sndpcm->period_size); + return -EINVAL; + } + + fprintf(bat->log, _("Get period size: %d buffer size: %d\n"), + (int) sndpcm->period_size, (int) sndpcm->buffer_size); + + err = snd_pcm_format_physical_width(format); + if (err < 0) { + fprintf(bat->err, _("Invalid parameters: ")); + fprintf(bat->err, _("snd_pcm_format_physical_width: %d\n"), + err); + return err; + } + sndpcm->sample_bits = err; + + sndpcm->frame_bits = sndpcm->sample_bits * bat->channels; + + /* Calculate the period bytes */ + sndpcm->period_bytes = sndpcm->period_size * sndpcm->frame_bits / 8; + sndpcm->buffer = (char *) malloc(sndpcm->period_bytes); + if (sndpcm->buffer == NULL) { + fprintf(bat->err, _("Not enough memory: size=%zd\n"), + sndpcm->period_bytes); + return -ENOMEM; + } + + return 0; +} + +static int write_to_pcm(const struct pcm_container *sndpcm, + int frames, struct bat *bat) +{ + int err; + int offset = 0; + int remain = frames; + + while (remain > 0) { + err = snd_pcm_writei(sndpcm->handle, sndpcm->buffer + offset, + remain); + if (err == -EAGAIN || (err >= 0 && err < frames)) { + snd_pcm_wait(sndpcm->handle, 500); + } else if (err == -EPIPE) { + fprintf(bat->err, _("Underrun: %s(%d)\n"), + snd_strerror(err), err); + if (bat->roundtriplatency) + bat->latency.xrun_error = true; + snd_pcm_prepare(sndpcm->handle); + } else if (err == -ESTRPIPE) { + while ((err = snd_pcm_resume(sndpcm->handle)) == -EAGAIN) + sleep(1); /* wait until resume flag is released */ + if (err < 0) + snd_pcm_prepare(sndpcm->handle); + } else if (err < 0) { + fprintf(bat->err, _("Write PCM device error: %s(%d)\n"), + snd_strerror(err), err); + return err; + } + + if (err > 0) { + remain -= err; + offset += err * sndpcm->frame_bits / 8; + } + } + + return 0; +} + +/** + * Process output data for latency test + */ +static int latencytest_process_output(struct pcm_container *sndpcm, + struct bat *bat) +{ + int err = 0; + int bytes = sndpcm->period_bytes; /* playback buffer size */ + int frames = sndpcm->period_size; /* frame count */ + + bat->latency.is_playing = true; + + while (1) { + /* generate output data */ + err = handleoutput(bat, sndpcm->buffer, bytes, frames); + if (err != 0) + break; + + err = write_to_pcm(sndpcm, frames, bat); + if (err != 0) + break; + + /* Xrun error, terminate the playback thread*/ + if (bat->latency.xrun_error == true) + break; + + if (bat->latency.state == LATENCY_STATE_COMPLETE_SUCCESS) + break; + + bat->periods_played++; + } + + bat->latency.is_playing = false; + + return err; +} + +static int write_to_pcm_loop(struct pcm_container *sndpcm, struct bat *bat) +{ + int err = 0; + int bytes = sndpcm->period_bytes; /* playback buffer size */ + int frames = bytes * 8 / sndpcm->frame_bits; /* frame count */ + FILE *fp = NULL; + int bytes_total = 0; + + if (bat->debugplay) { + fp = fopen(bat->debugplay, "wb"); + err = -errno; + if (fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->debugplay, err); + return err; + } + /* leave space for wav header */ + if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) { + err = -errno; + fclose(fp); + return err; + } + } + + while (1) { + err = generate_input_data(bat, sndpcm->buffer, bytes, frames); + if (err != 0) + break; + + if (bat->debugplay) { + if (fwrite(sndpcm->buffer, 1, bytes, fp) != (size_t)bytes) { + err = -EIO; + break; + } + bytes_total += bytes; + } + + bat->periods_played++; + if (bat->period_is_limited + && bat->periods_played >= bat->periods_total) + break; + + err = write_to_pcm(sndpcm, frames, bat); + if (err != 0) + break; + } + + if (bat->debugplay) { + update_wav_header(bat, fp, bytes_total); + fclose(fp); + } + + snd_pcm_drain(sndpcm->handle); + + return err; +} + +/** + * Play + */ +void *playback_alsa(struct bat *bat) +{ + int err = 0; + struct pcm_container sndpcm; + + fprintf(bat->log, _("Entering playback thread (ALSA).\n")); + + retval_play = 0; + memset(&sndpcm, 0, sizeof(sndpcm)); + + err = snd_pcm_open(&sndpcm.handle, bat->playback.device, + SND_PCM_STREAM_PLAYBACK, 0); + if (err != 0) { + fprintf(bat->err, _("Cannot open PCM playback device: ")); + fprintf(bat->err, _("%s(%d)\n"), snd_strerror(err), err); + retval_play = err; + goto exit1; + } + + err = set_snd_pcm_params(bat, &sndpcm); + if (err != 0) { + retval_play = err; + goto exit2; + } + + if (bat->playback.file == NULL) { + fprintf(bat->log, _("Playing generated audio sine wave")); + bat->sinus_duration == 0 ? + fprintf(bat->log, _(" endlessly\n")) : + fprintf(bat->log, _("\n")); + } else { + fprintf(bat->log, _("Playing input audio file: %s\n"), + bat->playback.file); + bat->fp = fopen(bat->playback.file, "rb"); + err = -errno; + if (bat->fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->playback.file, err); + retval_play = err; + goto exit3; + } + /* Skip header */ + err = read_wav_header(bat, bat->playback.file, bat->fp, true); + if (err != 0) { + retval_play = err; + goto exit4; + } + } + + if (bat->roundtriplatency) + err = latencytest_process_output(&sndpcm, bat); + else + err = write_to_pcm_loop(&sndpcm, bat); + if (err < 0) { + retval_play = err; + goto exit4; + } + +exit4: + if (bat->playback.file) + fclose(bat->fp); +exit3: + free(sndpcm.buffer); +exit2: + snd_pcm_close(sndpcm.handle); +exit1: + pthread_exit(&retval_play); +} + +static int read_from_pcm(struct pcm_container *sndpcm, + int frames, struct bat *bat) +{ + int err = 0; + int offset = 0; + int remain = frames; + + while (remain > 0) { + err = snd_pcm_readi(sndpcm->handle, + sndpcm->buffer + offset, remain); + if (err == -EAGAIN || (err >= 0 && err < remain)) { + snd_pcm_wait(sndpcm->handle, 500); + } else if (err == -EPIPE) { + snd_pcm_prepare(sndpcm->handle); + fprintf(bat->err, _("Overrun: %s(%d)\n"), + snd_strerror(err), err); + if (bat->roundtriplatency) + bat->latency.xrun_error = true; + } else if (err == -ESTRPIPE) { + while ((err = snd_pcm_resume(sndpcm->handle)) == -EAGAIN) + sleep(1); /* wait until resume flag is released */ + if (err < 0) + snd_pcm_prepare(sndpcm->handle); + } else if (err < 0) { + fprintf(bat->err, _("Read PCM device error: %s(%d)\n"), + snd_strerror(err), err); + return err; + } + + if (err > 0) { + remain -= err; + offset += err * sndpcm->frame_bits / 8; + } + } + + return 0; +} + +static int read_from_pcm_loop(struct pcm_container *sndpcm, struct bat *bat) +{ + int err = 0; + FILE *fp = NULL; + int size, frames; + int bytes_read = 0; + int bytes_count = bat->frames * bat->frame_size; + unsigned int remain = bytes_count; + + remove(bat->capture.file); + fp = fopen(bat->capture.file, "wb"); + err = -errno; + if (fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->capture.file, err); + return err; + } + /* leave space for file header */ + if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) { + err = -errno; + fclose(fp); + return err; + } + + while (remain > 0) { + size = (remain <= sndpcm->period_bytes) ? + remain : sndpcm->period_bytes; + frames = size * 8 / sndpcm->frame_bits; + + /* read a chunk from pcm device */ + err = read_from_pcm(sndpcm, frames, bat); + if (err != 0) + break; + + /* write the chunk to file */ + if (fwrite(sndpcm->buffer, 1, size, fp) != (size_t)size) { + err = -EIO; + break; + } + + bytes_read += size; + remain -= size; + bat->periods_played++; + + if (bat->period_is_limited + && bat->periods_played >= bat->periods_total) + break; + } + + update_wav_header(bat, fp, bytes_read); + + fclose(fp); + return err; +} + +/** + * Process input data for latency test + */ +static int latencytest_process_input(struct pcm_container *sndpcm, + struct bat *bat) +{ + int err = 0; + FILE *fp = NULL; + int bytes_read = 0; + int frames = sndpcm->period_size; + int size = sndpcm->period_bytes; + int bytes_count = bat->frames * bat->frame_size; + + remove(bat->capture.file); + fp = fopen(bat->capture.file, "wb"); + err = -errno; + if (fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->capture.file, err); + return err; + } + /* leave space for file header */ + if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) { + fclose(fp); + return err; + } + + bat->latency.is_capturing = true; + + while (bytes_read < bytes_count) { + /* read a chunk from pcm device */ + err = read_from_pcm(sndpcm, frames, bat); + if (err != 0) + break; + + /* Xrun error, terminate the capture thread*/ + if (bat->latency.xrun_error == true) + break; + + err = handleinput(bat, sndpcm->buffer, frames); + if (err != 0) + break; + + if (bat->latency.is_playing == false) + break; + + /* write the chunk to file */ + if (fwrite(sndpcm->buffer, 1, size, fp) != (size_t)size) { + err = -EIO; + break; + } + + bytes_read += size; + } + + bat->latency.is_capturing = false; + + update_wav_header(bat, fp, bytes_read); + + fclose(fp); + return err; +} + + +static void pcm_cleanup(void *p) +{ + snd_pcm_close(p); +} + +/** + * Record + */ +void *record_alsa(struct bat *bat) +{ + int err = 0; + struct pcm_container sndpcm; + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + + fprintf(bat->log, _("Entering capture thread (ALSA).\n")); + + retval_record = 0; + memset(&sndpcm, 0, sizeof(sndpcm)); + + err = snd_pcm_open(&sndpcm.handle, bat->capture.device, + SND_PCM_STREAM_CAPTURE, 0); + if (err != 0) { + fprintf(bat->err, _("Cannot open PCM capture device: ")); + fprintf(bat->err, _("%s(%d)\n"), snd_strerror(err), err); + retval_record = err; + goto exit1; + } + + err = set_snd_pcm_params(bat, &sndpcm); + if (err != 0) { + retval_record = err; + goto exit2; + } + + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); + pthread_cleanup_push(pcm_cleanup, sndpcm.handle); + pthread_cleanup_push(free, sndpcm.buffer); + + fprintf(bat->log, _("Recording ...\n")); + if (bat->roundtriplatency) + err = latencytest_process_input(&sndpcm, bat); + else + err = read_from_pcm_loop(&sndpcm, bat); + + pthread_cleanup_pop(0); + pthread_cleanup_pop(0); + + if (err != 0) { + retval_record = err; + goto exit3; + } + + /* Normally we will never reach this part of code (unless error in + * previous call) (before exit3) as this thread will be cancelled + * by end of play thread. Except in single line mode. */ + snd_pcm_drain(sndpcm.handle); + pthread_exit(&retval_record); + +exit3: + free(sndpcm.buffer); +exit2: + snd_pcm_close(sndpcm.handle); +exit1: + pthread_exit(&retval_record); +} diff --git a/bat/alsa.h b/bat/alsa.h new file mode 100644 index 0000000..d5c9972 --- /dev/null +++ b/bat/alsa.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +extern int retval_play; +extern int retval_record; + +void *playback_alsa(struct bat *); +void *record_alsa(struct bat *); diff --git a/bat/alsabat-test.sh b/bat/alsabat-test.sh new file mode 100755 index 0000000..bc3d512 --- /dev/null +++ b/bat/alsabat-test.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +# default devices +dev_playback="default" +dev_capture="default" + +bin="alsabat" +commands="$bin -P $dev_playback -C $dev_capture" + +file_sin_mono="default_mono.wav" +file_sin_dual="default_dual.wav" +logdir="tmp" + +# frequency range of signal +maxfreq=16547 +minfreq=17 + +# sleep time and pause time +sleep_time=5 +pause_time=2 + +# features passes vs. features all +feature_pass=0 +feature_cnt=0 + +init_counter () { + feature_pass=0 + feature_all=0 +} + +evaluate_result () { + feature_cnt=$((feature_cnt+1)) + if [ $1 -eq 0 ]; then + feature_pass=$((feature_pass+1)) + echo "pass" + else + echo "fail" + fi +} + +print_result () { + echo "[$feature_pass/$feature_cnt] features passes." +} + +feature_test () { + echo "============================================" + echo "$feature_cnt: ALSA $2" + echo "-------------------------------------------" + echo "$commands $1 --log=$logdir/$feature_cnt.log" + $commands $1 --log=$logdir/$feature_cnt.log + evaluate_result $? + echo "$commands $1" >> $logdir/$((feature_cnt-1)).log +} + +feature_test_power () { + echo "============================================" + echo "$feature_cnt: ALSA $2" + echo "-------------------------------------------" + echo "$commands $1 --log=$logdir/$feature_cnt.log" + + # run alsabat in the background + nohup $commands $1 > $logdir/$feature_cnt.log 2>&1 & + sleep $pause_time + pid=`ps -aux |grep alsabat|head -1 |awk -F ' ' '{print $2}'` + + # stop the alsabat thread + kill -STOP $pid > /dev/null + sleep 4 + + # do system S3 + rtcwake -m mem -s $sleep_time + sleep $pause_time + + # resume the alasbat thread to run + kill -CONT $pid > /dev/null + + # wait for alsabat to complete the analysis + sleep $pause_time + cat $logdir/$feature_cnt.log |grep -i "Return value is 0" > /dev/null + evaluate_result $? + echo "$commands $1" >> $logdir/$((feature_cnt-1)).log +} + +# test items +feature_list_test () { + init_counter + + commands="$bin" + feature_test "-c1 --saveplay $file_sin_mono" \ + "generate mono wav file with default params" + feature_test "-c2 --saveplay $file_sin_dual" \ + "generate dual wav file with default params" + sleep 5 + feature_test "-P $dev_playback" "single line mode, playback" + feature_test "-C $dev_capture --standalone" "single line mode, capture" + + commands="$bin -P $dev_playback -C $dev_capture" + feature_test "--file $file_sin_mono" "play mono wav file and detect" + feature_test "--file $file_sin_dual" "play dual wav file and detect" + feature_test "-c1" "configurable channel number: 1" + feature_test "-c2 -F $minfreq:$maxfreq" "configurable channel number: 2" + feature_test "-r44100" "configurable sample rate: 44100" + feature_test "-r48000" "configurable sample rate: 48000" + feature_test "-n10000" "configurable duration: in samples" + feature_test "-n2.5s" "configurable duration: in seconds" + feature_test "-f U8" "configurable data format: U8" + feature_test "-f S16_LE" "configurable data format: S16_LE" + feature_test "-f S24_3LE" "configurable data format: S24_3LE" + feature_test "-f S32_LE" "configurable data format: S32_LE" + feature_test "-f cd" "configurable data format: cd" + feature_test "-f dat" "configurable data format: dat" + feature_test "-F $maxfreq --standalone" \ + "standalone mode: play and capture" + latestfile=`ls -t1 /tmp/bat.wav.* | head -n 1` + feature_test "--local -F $maxfreq --file $latestfile" \ + "local mode: analyze local file" + feature_test "--roundtriplatency" \ + "round trip latency test" + feature_test "--snr-db 26" \ + "noise detect threshold in SNR(dB)" + feature_test "--snr-pc 5" \ + "noise detect threshold in noise percentage(%)" + feature_test_power "-n5s" "power management: S3 test" + + print_result +} + +echo "*******************************************" +echo " BAT Test " +echo "-------------------------------------------" + +# get device +echo "usage:" +echo " $0 " +echo " $0 " + +if [ $# -eq 2 ]; then + dev_playback=$1 + dev_capture=$2 +elif [ $# -eq 1 ]; then + dev_playback=$1 + dev_capture=$1 +fi + +echo "current setting:" +echo " $0 $dev_playback $dev_capture" + +# run +mkdir -p $logdir +feature_list_test + +echo "*******************************************" diff --git a/bat/alsabat.1 b/bat/alsabat.1 new file mode 100644 index 0000000..67b6ca8 --- /dev/null +++ b/bat/alsabat.1 @@ -0,0 +1,185 @@ +.TH ALSABAT 1 "20th October 2015" +.SH NAME +alsabat \- command\-line sound tester for ALSA sound card driver + +.SH SYNOPSIS +\fBalsabat\fP [\fIflags\fP] + +.SH DESCRIPTION +\fBALSABAT(ALSA Basic Audio Tester)\fP is a simple command\-line utility +intended to help automate audio driver and sound server testing with little +human interaction. ALSABAT can be used to test audio quality, stress test +features and test audio before and after PM state changes. + +ALSABAT's design is relatively simple. ALSABAT plays an audio stream and +captures the same stream in either a digital or analog loop back. It then +compares the captured stream using a FFT to the original to determine if +the test case passes or fails. + +ALSABAT can either run wholly on the target machine being tested (standalone +mode) or can run as a client/server mode where by alsabat client runs on the +target and runs as a server on a separate tester machine. The client/server +mode still requires some manual interaction for synchronization, but this +is actively being developed for future releases. + +The hardware testing configuration may require the use of an analog cable +connecting target to tester machines or a cable to create an analog +loopback if no loopback mode is available on the sound hardware that is +being tested. +An analog loopback cable can be used to connect the "line in" to "line out" +jacks to create a loopback. If only headphone and mic jacks (or combo jack) +are available then the following simple circuit can be used to create an +analog loopback :- + +https://source.android.com/devices/audio/loopback.html + +If tinyalsa is installed in system, user can choose tinyalsa as backend lib +of alsabat, with configure option "--enable-alsabat-backend-tiny". + +.SH OPTIONS +.TP +\fI\-h, \-\-help\fP +Help: show syntax. +.TP +\fI\-D\fP +Select sound card to be tested by name. +.TP +\fI\-P\fP +Select the playback PCM device. +.TP +\fI\-C\fP +Select the capture PCM device. +.TP +\fI\-f\fP +Sample format +.br +Recognized sample formats are: U8 S16_LE S24_3LE S32_LE +.br +Some of these may not be available on selected hardware +.br +The available format shortcuts are: +.nf +\-f cd (16 bit little endian, 44100, stereo) [\-f S16_LE \-c2 \-r44100] +\-f dat (16 bit little endian, 48000, stereo) [\-f S16_LE \-c2 \-r48000] +.fi +If no format is given S16_LE is used. +.TP +\fI\-c\fP +The number of channels. The default is one channel. +Valid values at the moment are 1 or 2. +.TP +\fI\-r\fP +Sampling rate in Hertz. The default rate is 44100 Hertz. +Valid values depends on hardware support. +.TP +\fI\-n\fP +Duration of generated signal. +The value could be either of the two forms: +.br +1. Decimal integer, means number of frames; +.br +2. Floating point with suffix 's', means number of seconds. +.br +The default is 2 seconds. +.TP +\fI\-k\fP +Sigma k value for analysis. +.br +The analysis function reads data from WAV file, run FFT against the data +to get magnitude of frequency vectors, and then calculates the average +value and standard deviation of frequency vectors. After that, we define +a threshold: +.br +threshold = k * standard_deviation + mean_value +.br +Frequencies with amplitude larger than threshold will be recognized as a +peak, and the frequency with largest peak value will be recognized as a +detected frequency. +.br +ALSABAT then compares the detected frequency to target frequency, to +decide if the detecting passes or fails. +.br +The default value is 3.0. +.TP +\fI\-F\fP +Target frequency for signal generation and analysis, in Hertz. +The default is 997.0 Hertz. +Valid range is (DC_THRESHOLD, 40% * Sampling rate). +.TP +\fI\-p\fP +Total number of periods to play or capture. +.TP +\fI\-\-log=#\fP +Write stderr and stdout output to this log file. +.TP +\fI\-\-file=#\fP +Input WAV file for playback. +.TP +\fI\-\-saveplay=#\fP +Target WAV file to save capture test content. +.TP +\fI\-\-local\fP +Internal loopback mode. +Playback, capture and analysis internal to ALSABAT only. This is intended +for developers to test new ALSABAT features as no audio is routed outside +of ALSABAT. +.TP +\fI\-\-standalone\fP +Add support for standalone mode where ALSABAT will run on a different machine +to the one being tested. +In standalone mode, the sound data can be generated, playback and captured +just like in normal mode, but will not be analyzed. +The ALSABAT being built without libfftw3 support is always in standalone mode. +The ALSABAT in normal mode can also bypass data analysis using option +"--standalone". +.TP +\fI\-\-roundtriplatency\fP +Round trip latency test. +Audio latency is the time delay as an audio signal passes through a system. +There are many kinds of audio latency metrics. One useful metric is the +round trip latency, which is the sum of output latency and input latency. +.TP +\fI\-\-snr\-db=#\fP +Noise detection threshold in SNR (dB). 26dB indicates 5% noise in amplitude. +ALSABAT will return error if signal SNR is smaller than the threshold. +.TP +\fI\-\-snr\-pc=#\fP +Noise detection threshold in percentage of noise amplitude (%). +ALSABAT will return error if the noise amplitude is larger than the threshold. + +.SH EXAMPLES + +.TP +\fBalsabat \-P plughw:0,0 \-C plughw:0,0 \-c 2 \-f S32_LE \-F 250\fR +Generate and play a sine wave of 250 Hertz with 2 channel and S32_LE format, +and then capture and analyze. + +.TP +\fBalsabat \-P plughw:0,0 \-C plughw:0,0 \-\-file 500Hz.wav\fR +Play the RIFF WAV file "500Hz.wav" which contains 500 Hertz waveform LPCM +data, and then capture and analyze. + +.SH RETURN VALUE +.br +On success, returns 0. +.br +If no peak be detected, returns -1001; +.br +If only DC be detected, returns -1002; +.br +If peak frequency does not match with the target frequency, returns -1003. + +.SH SEE ALSO +\fB +aplay(1) +\fP + +.SH BUGS +Currently only support RIFF WAV format with PCM data. Please report any bugs to +the alsa-devel mailing list. + +.SH AUTHOR +\fBalsabat\fP is by Liam Girdwood , Bernard +Gautier and Han Lu . +This document is by Liam Girdwood and Han Lu +. diff --git a/bat/analyze.c b/bat/analyze.c new file mode 100644 index 0000000..b8d0270 --- /dev/null +++ b/bat/analyze.c @@ -0,0 +1,514 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "gettext.h" + +#include "common.h" +#include "bat-signal.h" + +static void check_amplitude(struct bat *bat, float *buf) +{ + float sum, average, amplitude; + int i, percent; + + /* calculate average value */ + for (i = 0, sum = 0.0, average = 0.0; i < bat->frames; i++) + sum += buf[i]; + average = sum / bat->frames; + + /* calculate peak-to-average amplitude */ + for (i = 0, sum = 0.0; i < bat->frames; i++) + sum += fabsf(buf[i] - average); + amplitude = sum / bat->frames * M_PI / 2.0; + + /* calculate amplitude percentage against full range */ + percent = amplitude * 100 / ((1 << ((bat->sample_size << 3) - 1)) - 1); + + fprintf(bat->log, _("Amplitude: %.1f; Percentage: [%d]\n"), + amplitude, percent); + if (percent < 0) + fprintf(bat->err, _("ERROR: Amplitude can't be negative!\n")); + else if (percent < 1) + fprintf(bat->err, _("WARNING: Signal too weak!\n")); + else if (percent > 100) + fprintf(bat->err, _("WARNING: Signal overflow!\n")); +} + +/** + * + * @return 0 if peak detected at right frequency, + * 1 if peak detected somewhere else + * 2 if DC detected + */ +int check_peak(struct bat *bat, struct analyze *a, int end, int peak, float hz, + float mean, float p, int channel, int start) +{ + int err; + float hz_peak = (float) (peak) * hz; + float delta_rate = DELTA_RATE * bat->target_freq[channel]; + float delta_HZ = DELTA_HZ; + float tolerance = (delta_rate > delta_HZ) ? delta_rate : delta_HZ; + + fprintf(bat->log, _("Detected peak at %2.2f Hz of %2.2f dB\n"), hz_peak, + 10.0 * log10f(a->mag[peak] / mean)); + fprintf(bat->log, _(" Total %3.1f dB from %2.2f to %2.2f Hz\n"), + 10.0 * log10f(p / mean), start * hz, end * hz); + + if (hz_peak < DC_THRESHOLD) { + fprintf(bat->err, _(" WARNING: Found low peak %2.2f Hz,"), + hz_peak); + fprintf(bat->err, _(" very close to DC\n")); + err = FOUND_DC; + } else if (hz_peak < bat->target_freq[channel] - tolerance) { + fprintf(bat->err, _(" FAIL: Peak freq too low %2.2f Hz\n"), + hz_peak); + err = FOUND_WRONG_PEAK; + } else if (hz_peak > bat->target_freq[channel] + tolerance) { + fprintf(bat->err, _(" FAIL: Peak freq too high %2.2f Hz\n"), + hz_peak); + err = FOUND_WRONG_PEAK; + } else { + fprintf(bat->log, _(" PASS: Peak detected")); + fprintf(bat->log, _(" at target frequency\n")); + err = 0; + } + + return err; +} + +/** + * Search for main frequencies in fft results and compare it to target + */ +static int check(struct bat *bat, struct analyze *a, int channel) +{ + float hz = 1.0 / ((float) bat->frames / (float) bat->rate); + float mean = 0.0, t, sigma = 0.0, p = 0.0; + int i, start = -1, end = -1, peak = 0, signals = 0; + int err = 0, N = bat->frames / 2; + + /* calculate mean */ + for (i = 0; i < N; i++) + mean += a->mag[i]; + mean /= (float) N; + + /* calculate standard deviation */ + for (i = 0; i < N; i++) { + t = a->mag[i] - mean; + t *= t; + sigma += t; + } + sigma /= (float) N; + sigma = sqrtf(sigma); + + /* clip any data less than k sigma + mean */ + for (i = 0; i < N; i++) { + if (a->mag[i] > mean + bat->sigma_k * sigma) { + + /* find peak start points */ + if (start == -1) { + start = peak = end = i; + signals++; + } else { + if (a->mag[i] > a->mag[peak]) + peak = i; + end = i; + } + p += a->mag[i]; + } else if (start != -1) { + /* Check if peak is as expected */ + err |= check_peak(bat, a, end, peak, hz, mean, + p, channel, start); + end = start = -1; + if (signals == MAX_PEAKS) + break; + } + } + if (signals == 0) + err = -ENOPEAK; /* No peak detected */ + else if ((err == FOUND_DC) && (signals == 1)) + err = -EONLYDC; /* Only DC detected */ + else if ((err & FOUND_WRONG_PEAK) == FOUND_WRONG_PEAK) + err = -EBADPEAK; /* Bad peak detected */ + else + err = 0; /* Correct peak detected */ + + fprintf(bat->log, _("Detected at least %d signal(s) in total\n"), + signals); + + return err; +} + +static void calc_magnitude(struct bat *bat, struct analyze *a, int N) +{ + float r2, i2; + int i; + + for (i = 1; i < N / 2; i++) { + r2 = a->out[i] * a->out[i]; + i2 = a->out[N - i] * a->out[N - i]; + + a->mag[i] = sqrtf(r2 + i2); + } + a->mag[0] = 0.0; +} + +static int find_and_check_harmonics(struct bat *bat, struct analyze *a, + int channel) +{ + fftwf_plan p; + int err = -ENOMEM, N = bat->frames; + + /* Allocate FFT buffers */ + a->in = (float *) fftwf_malloc(sizeof(float) * bat->frames); + if (a->in == NULL) + goto out1; + + a->out = (float *) fftwf_malloc(sizeof(float) * bat->frames); + if (a->out == NULL) + goto out2; + + a->mag = (float *) fftwf_malloc(sizeof(float) * bat->frames); + if (a->mag == NULL) + goto out3; + + /* create FFT plan */ + p = fftwf_plan_r2r_1d(N, a->in, a->out, FFTW_R2HC, + FFTW_MEASURE | FFTW_PRESERVE_INPUT); + if (p == NULL) + goto out4; + + /* convert source PCM to floats */ + bat->convert_sample_to_float(a->buf, a->in, bat->frames); + + /* check amplitude */ + check_amplitude(bat, a->in); + + /* run FFT */ + fftwf_execute(p); + + /* FFT out is real and imaginary numbers - calc magnitude for each */ + calc_magnitude(bat, a, N); + + /* check data */ + err = check(bat, a, channel); + + fftwf_destroy_plan(p); + +out4: + fftwf_free(a->mag); +out3: + fftwf_free(a->out); +out2: + fftwf_free(a->in); +out1: + return err; +} + +static int calculate_noise_one_period(struct bat *bat, + struct noise_analyzer *na, float *src, + int length, int channel) +{ + int i, shift = 0; + float tmp, rms, gain, residual; + float a = 0.0, b = 1.0; + + /* step 1. phase compensation */ + + if (length < 2 * na->nsamples) + return -EINVAL; + + /* search for the beginning of a sine period */ + for (i = 0, tmp = 0.0, shift = -1; i < na->nsamples; i++) { + /* find i where src[i] >= 0 && src[i+1] < 0 */ + if (src[i] < 0.0) + continue; + if (src[i + 1] < 0.0) { + tmp = src[i] - src[i + 1]; + a = src[i] / tmp; + b = -src[i + 1] / tmp; + shift = i; + break; + } + } + + /* didn't find the beginning of a sine period */ + if (shift == -1) + return -EINVAL; + + /* shift sine waveform to source[0] = 0.0 */ + for (i = 0; i < na->nsamples; i++) + na->source[i] = a * src[i + shift + 1] + b * src[i + shift]; + + /* step 2. gain compensation */ + + /* calculate rms of signal amplitude */ + for (i = 0, tmp = 0.0; i < na->nsamples; i++) + tmp += na->source[i] * na->source[i]; + rms = sqrtf(tmp / na->nsamples); + + gain = na->rms_tgt / rms; + + for (i = 0; i < na->nsamples; i++) + na->source[i] *= gain; + + /* step 3. calculate snr in dB */ + + for (i = 0, tmp = 0.0, residual = 0.0; i < na->nsamples; i++) { + tmp = fabsf(na->target[i] - na->source[i]); + residual += tmp * tmp; + } + + tmp = na->rms_tgt / sqrtf(residual / na->nsamples); + na->snr_db = 20.0 * log10f(tmp); + + return 0; +} + +static int calculate_noise(struct bat *bat, float *src, int channel) +{ + int err = 0; + struct noise_analyzer na; + float freq = bat->target_freq[channel]; + float tmp, sum_snr_pc, avg_snr_pc, avg_snr_db; + int offset, i, cnt_noise, cnt_clean; + /* num of samples in each sine period */ + int nsamples = (int) ceilf(bat->rate / freq); + /* each section has 2 sine periods, the first one for locating + * and the second one for noise calculating */ + int nsamples_per_section = nsamples * 2; + /* all sine periods will be calculated except the first and last one */ + int nsection = bat->frames / nsamples - 1; + + fprintf(bat->log, _("samples per period: %d\n"), nsamples); + fprintf(bat->log, _("total sections to detect: %d\n"), nsection); + na.source = (float *)malloc(sizeof(float) * nsamples); + if (!na.source) { + err = -ENOMEM; + goto out1; + } + + na.target = (float *)malloc(sizeof(float) * nsamples); + if (!na.target) { + err = -ENOMEM; + goto out2; + } + + /* generate standard single-tone signal */ + err = generate_sine_wave_raw_mono(bat, na.target, freq, nsamples); + if (err < 0) + goto out3; + + na.nsamples = nsamples; + + /* calculate rms of standard signal */ + for (i = 0, tmp = 0.0; i < nsamples; i++) + tmp += na.target[i] * na.target[i]; + na.rms_tgt = sqrtf(tmp / nsamples); + + /* calculate average noise level */ + sum_snr_pc = 0.0; + cnt_clean = cnt_noise = 0; + for (i = 1, offset = nsamples; i < nsection; i++) { + na.snr_db = SNR_DB_INVALID; + + err = calculate_noise_one_period(bat, &na, src + offset, + nsamples_per_section, channel); + if (err < 0) + goto out3; + + if (na.snr_db > bat->snr_thd_db) { + cnt_clean++; + sum_snr_pc += 100.0 / powf(10.0, na.snr_db / 20.0); + } else { + cnt_noise++; + } + offset += nsamples; + } + + if (cnt_noise > 0) { + fprintf(bat->err, _("Noise detected at %d points.\n"), + cnt_noise); + err = -cnt_noise; + if (cnt_clean == 0) + goto out3; + } else { + fprintf(bat->log, _("No noise detected.\n")); + } + + avg_snr_pc = sum_snr_pc / cnt_clean; + avg_snr_db = 20.0 * log10f(100.0 / avg_snr_pc); + fprintf(bat->log, _("Average SNR is %.2f dB (%.2f %%) at %d points.\n"), + avg_snr_db, avg_snr_pc, cnt_clean); + +out3: + free(na.target); +out2: + free(na.source); +out1: + return err; +} + +static int find_and_check_noise(struct bat *bat, void *buf, int channel) +{ + int err = 0; + float *source; + + source = (float *)malloc(sizeof(float) * bat->frames); + if (!source) + return -ENOMEM; + + /* convert source PCM to floats */ + bat->convert_sample_to_float(buf, source, bat->frames); + + /* adjust waveform and calculate noise */ + err = calculate_noise(bat, source, channel); + + free(source); + return err; +} + +/** + * Convert interleaved samples from channels in samples from a single channel + */ +static int reorder_data(struct bat *bat) +{ + char *p, *new_bat_buf; + int ch, i, j; + + if (bat->channels == 1) + return 0; /* No need for reordering */ + + p = malloc(bat->frames * bat->frame_size); + new_bat_buf = p; + if (p == NULL) + return -ENOMEM; + + for (ch = 0; ch < bat->channels; ch++) { + for (j = 0; j < bat->frames; j++) { + for (i = 0; i < bat->sample_size; i++) { + *p++ = ((char *) (bat->buf))[j * bat->frame_size + + ch * bat->sample_size + i]; + } + } + } + + free(bat->buf); + bat->buf = new_bat_buf; + + return 0; +} + +/* truncate sample frames for faster FFT analysis process */ +static int truncate_frames(struct bat *bat) +{ + int shift = SHIFT_MAX; + + for (; shift > SHIFT_MIN; shift--) + if (bat->frames & (1 << shift)) { + bat->frames = 1 << shift; + return 0; + } + + return -EINVAL; +} + +int analyze_capture(struct bat *bat) +{ + int err = 0; + size_t items; + int c; + struct analyze a; + + err = truncate_frames(bat); + if (err < 0) { + fprintf(bat->err, _("Invalid frame number for analysis: %d\n"), + bat->frames); + return err; + } + + fprintf(bat->log, _("\nBAT analysis: signal has %d frames at %d Hz,"), + bat->frames, bat->rate); + fprintf(bat->log, _(" %d channels, %d bytes per sample.\n"), + bat->channels, bat->sample_size); + + bat->buf = malloc(bat->frames * bat->frame_size); + if (bat->buf == NULL) + return -ENOMEM; + + bat->fp = fopen(bat->capture.file, "rb"); + err = -errno; + if (bat->fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->capture.file, err); + goto exit1; + } + + /* Skip header */ + err = read_wav_header(bat, bat->capture.file, bat->fp, true); + if (err != 0) + goto exit2; + + items = fread(bat->buf, bat->frame_size, bat->frames, bat->fp); + if (items != bat->frames) { + err = -EIO; + goto exit2; + } + + err = reorder_data(bat); + if (err != 0) + goto exit2; + + for (c = 0; c < bat->channels; c++) { + fprintf(bat->log, _("\nChannel %i - "), c + 1); + fprintf(bat->log, _("Checking for target frequency %2.2f Hz\n"), + bat->target_freq[c]); + a.buf = bat->buf + + c * bat->frames * bat->frame_size + / bat->channels; + if (!bat->standalone) { + err = find_and_check_harmonics(bat, &a, c); + if (err != 0) + goto exit2; + } + + if (snr_is_valid(bat->snr_thd_db)) { + fprintf(bat->log, _("\nChecking for SNR: ")); + fprintf(bat->log, _("Threshold is %.2f dB (%.2f%%)\n"), + bat->snr_thd_db, 100.0 + / powf(10.0, bat->snr_thd_db / 20.0)); + err = find_and_check_noise(bat, a.buf, c); + if (err != 0) + goto exit2; + } + } + +exit2: + fclose(bat->fp); +exit1: + free(bat->buf); + + return err; +} diff --git a/bat/analyze.h b/bat/analyze.h new file mode 100644 index 0000000..3fd03d4 --- /dev/null +++ b/bat/analyze.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +int analyze_capture(struct bat *); diff --git a/bat/bat-signal.h b/bat/bat-signal.h new file mode 100644 index 0000000..3c314a5 --- /dev/null +++ b/bat/bat-signal.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 Caleb Crome + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +/* + * Here's a generic sine wave generator that will work indefinitely + * for any frequency. + * + * Note: the state & phasor are stored as doubles (and updated as + * doubles) because after a million samples the magnitude drifts a + * bit. If we really need floats, it can be done with periodic + * renormalization of the state_real+state_imag magnitudes. + */ + +int sin_generator_init(struct sin_generator *, float, float, float); +float sin_generator_next_sample(struct sin_generator *); +void sin_generator_vfill(struct sin_generator *, float *, int); +int generate_sine_wave(struct bat *, int, void *); +int generate_sine_wave_raw_mono(struct bat *, float *, float, int); diff --git a/bat/bat.c b/bat/bat.c new file mode 100644 index 0000000..ea04ed9 --- /dev/null +++ b/bat/bat.c @@ -0,0 +1,747 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aconfig.h" +#include "gettext.h" +#include "version.h" + +#include "common.h" + +#ifdef HAVE_LIBTINYALSA +#include "tinyalsa.h" +#else +#include "alsa.h" +#endif +#include "convert.h" +#ifdef HAVE_LIBFFTW3F +#include "analyze.h" +#endif +#include "latencytest.h" + +/* get snr threshold in dB */ +static void get_snr_thd_db(struct bat *bat, char *thd) +{ + int err; + float thd_db; + char *ptrf; + + thd_db = strtof(thd, &ptrf); + err = -errno; + if (!snr_is_valid(thd_db)) { + fprintf(bat->err, _("Invalid threshold '%s':%d\n"), thd, err); + exit(EXIT_FAILURE); + } + bat->snr_thd_db = thd_db; +} + +/* get snr threshold in %, and convert to dB */ +static void get_snr_thd_pc(struct bat *bat, char *thd) +{ + int err; + float thd_pc; + char *ptrf; + + thd_pc = strtof(thd, &ptrf); + err = -errno; + if (thd_pc <= 0.0 || thd_pc >= 100.0) { + fprintf(bat->err, _("Invalid threshold '%s':%d\n"), thd, err); + exit(EXIT_FAILURE); + } + bat->snr_thd_db = 20.0 * log10f(100.0 / thd_pc); +} + +static int get_duration(struct bat *bat) +{ + int err; + float duration_f; + long duration_i; + char *ptrf, *ptri; + + duration_f = strtof(bat->narg, &ptrf); + err = -errno; + if (duration_f == HUGE_VALF || duration_f == -HUGE_VALF + || (duration_f == 0.0 && err != 0)) + goto err_exit; + + duration_i = strtol(bat->narg, &ptri, 10); + if (duration_i == LONG_MAX || duration_i == LONG_MIN) + goto err_exit; + + if (*ptrf == 's') + bat->frames = duration_f * bat->rate; + else if (*ptri == 0) + bat->frames = duration_i; + else + bat->frames = -1; + + if (bat->frames <= 0 || bat->frames > MAX_FRAMES) { + fprintf(bat->err, _("Invalid duration. Range: (0, %d(%fs))\n"), + MAX_FRAMES, (float)MAX_FRAMES / bat->rate); + return -EINVAL; + } + + return 0; + +err_exit: + fprintf(bat->err, _("Duration overflow/underflow: %d\n"), err); + + return err; +} + +static void get_sine_frequencies(struct bat *bat, char *freq) +{ + char *tmp1; + + tmp1 = strchr(freq, ':'); + if (tmp1 == NULL) { + bat->target_freq[1] = bat->target_freq[0] = atof(optarg); + } else { + *tmp1 = '\0'; + bat->target_freq[0] = atof(optarg); + bat->target_freq[1] = atof(tmp1 + 1); + } +} + +static void get_format(struct bat *bat, char *optarg) +{ + if (strcasecmp(optarg, "cd") == 0) { + bat->format = BAT_PCM_FORMAT_S16_LE; + bat->rate = 44100; + bat->channels = 2; + bat->sample_size = 2; + } else if (strcasecmp(optarg, "dat") == 0) { + bat->format = BAT_PCM_FORMAT_S16_LE; + bat->rate = 48000; + bat->channels = 2; + bat->sample_size = 2; + } else if (strcasecmp(optarg, "U8") == 0) { + bat->format = BAT_PCM_FORMAT_U8; + bat->sample_size = 1; + } else if (strcasecmp(optarg, "S16_LE") == 0) { + bat->format = BAT_PCM_FORMAT_S16_LE; + bat->sample_size = 2; + } else if (strcasecmp(optarg, "S24_3LE") == 0) { + bat->format = BAT_PCM_FORMAT_S24_3LE; + bat->sample_size = 3; + } else if (strcasecmp(optarg, "S32_LE") == 0) { + bat->format = BAT_PCM_FORMAT_S32_LE; + bat->sample_size = 4; + } else { + bat->format = BAT_PCM_FORMAT_UNKNOWN; + fprintf(bat->err, _("wrong extended format '%s'\n"), optarg); + exit(EXIT_FAILURE); + } +} + +static inline int thread_wait_completion(struct bat *bat ATTRIBUTE_UNUSED, + pthread_t id, int **val) +{ + int err; + + err = pthread_join(id, (void **) val); + if (err) + pthread_cancel(id); + + return err; +} + +/* loopback test where we play sine wave and capture the same sine wave */ +static void test_loopback(struct bat *bat) +{ + pthread_t capture_id, playback_id; + int err; + int *thread_result_capture, *thread_result_playback; + + /* start playback */ + err = pthread_create(&playback_id, NULL, + (void *) bat->playback.fct, bat); + if (err != 0) { + fprintf(bat->err, _("Cannot create playback thread: %d\n"), + err); + exit(EXIT_FAILURE); + } + + /* TODO: use a pipe to signal stream start etc - i.e. to sync threads */ + /* Let some time for playing something before capturing */ + usleep(CAPTURE_DELAY * 1000); + + /* start capture */ + err = pthread_create(&capture_id, NULL, (void *) bat->capture.fct, bat); + if (err != 0) { + fprintf(bat->err, _("Cannot create capture thread: %d\n"), err); + pthread_cancel(playback_id); + exit(EXIT_FAILURE); + } + + /* wait for playback to complete */ + err = thread_wait_completion(bat, playback_id, &thread_result_playback); + if (err != 0) { + fprintf(bat->err, _("Cannot join playback thread: %d\n"), err); + free(thread_result_playback); + pthread_cancel(capture_id); + exit(EXIT_FAILURE); + } + + /* check playback status */ + if (*thread_result_playback != 0) { + fprintf(bat->err, _("Exit playback thread fail: %d\n"), + *thread_result_playback); + pthread_cancel(capture_id); + exit(EXIT_FAILURE); + } else { + fprintf(bat->log, _("Playback completed.\n")); + } + + /* now stop and wait for capture to finish */ + pthread_cancel(capture_id); + err = thread_wait_completion(bat, capture_id, &thread_result_capture); + if (err != 0) { + fprintf(bat->err, _("Cannot join capture thread: %d\n"), err); + free(thread_result_capture); + exit(EXIT_FAILURE); + } + + /* check if capture thread is canceled or not */ + if (thread_result_capture == PTHREAD_CANCELED) { + fprintf(bat->log, _("Capture canceled.\n")); + return; + } + + /* check capture status */ + if (*thread_result_capture != 0) { + fprintf(bat->err, _("Exit capture thread fail: %d\n"), + *thread_result_capture); + exit(EXIT_FAILURE); + } else { + fprintf(bat->log, _("Capture completed.\n")); + } +} + +/* single ended playback only test */ +static void test_playback(struct bat *bat) +{ + pthread_t playback_id; + int err; + int *thread_result; + + /* start playback */ + err = pthread_create(&playback_id, NULL, + (void *) bat->playback.fct, bat); + if (err != 0) { + fprintf(bat->err, _("Cannot create playback thread: %d\n"), + err); + exit(EXIT_FAILURE); + } + + /* wait for playback to complete */ + err = thread_wait_completion(bat, playback_id, &thread_result); + if (err != 0) { + fprintf(bat->err, _("Cannot join playback thread: %d\n"), err); + free(thread_result); + exit(EXIT_FAILURE); + } + + /* check playback status */ + if (*thread_result != 0) { + fprintf(bat->err, _("Exit playback thread fail: %d\n"), + *thread_result); + exit(EXIT_FAILURE); + } else { + fprintf(bat->log, _("Playback completed.\n")); + } +} + +/* single ended capture only test */ +static void test_capture(struct bat *bat) +{ + pthread_t capture_id; + int err; + int *thread_result; + + /* start capture */ + err = pthread_create(&capture_id, NULL, (void *) bat->capture.fct, bat); + if (err != 0) { + fprintf(bat->err, _("Cannot create capture thread: %d\n"), err); + exit(EXIT_FAILURE); + } + + /* TODO: stop capture */ + + /* wait for capture to complete */ + err = thread_wait_completion(bat, capture_id, &thread_result); + if (err != 0) { + fprintf(bat->err, _("Cannot join capture thread: %d\n"), err); + free(thread_result); + exit(EXIT_FAILURE); + } + + /* check playback status */ + if (*thread_result != 0) { + fprintf(bat->err, _("Exit capture thread fail: %d\n"), + *thread_result); + exit(EXIT_FAILURE); + } else { + fprintf(bat->log, _("Capture completed.\n")); + } +} + +static void usage(struct bat *bat) +{ + fprintf(bat->log, +_("Usage: alsabat [-options]...\n" +"\n" +" -h, --help this help\n" +" -D pcm device for both playback and capture\n" +" -P pcm device for playback\n" +" -C pcm device for capture\n" +" -f sample format\n" +" -c number of channels\n" +" -r sampling rate\n" +" -n frames to playback or capture\n" +" -k parameter for frequency detecting threshold\n" +" -F target frequency\n" +" -p total number of periods to play/capture\n" +" -B buffer size in frames\n" +" -E period size in frames\n" +" --log=# file that both stdout and strerr redirecting to\n" +" --file=# file for playback\n" +" --saveplay=# file that storing playback content, for debug\n" +" --readcapture=# file with previously captured content. File data\n" +" is used for analysis instead of capturing it.\n" +" --local internal loop, set to bypass pcm hardware devices\n" +" --standalone standalone mode, to bypass analysis\n" +" --roundtriplatency round trip latency mode\n" +" --snr-db=# noise detect threshold, in SNR(dB)\n" +" --snr-pc=# noise detect threshold, in noise percentage(%%)\n" +)); + fprintf(bat->log, _("Recognized sample formats are: ")); + fprintf(bat->log, _("U8 S16_LE S24_3LE S32_LE\n")); + fprintf(bat->log, _("The available format shotcuts are:\n")); + fprintf(bat->log, _("-f cd (16 bit little endian, 44100, stereo)\n")); + fprintf(bat->log, _("-f dat (16 bit little endian, 48000, stereo)\n")); +} + +static void set_defaults(struct bat *bat) +{ + memset(bat, 0, sizeof(struct bat)); + + /* Set default values */ + bat->rate = 44100; + bat->frame_size = 2; + bat->sample_size = 2; + bat->format = BAT_PCM_FORMAT_S16_LE; + bat->convert_float_to_sample = convert_float_to_int16; + bat->convert_sample_to_float = convert_int16_to_float; + bat->frames = bat->rate * 2; + bat->target_freq[0] = 997.0; + bat->target_freq[1] = 997.0; + bat->sigma_k = 3.0; + bat->snr_thd_db = SNR_DB_INVALID; + bat->playback.device = NULL; + bat->capture.device = NULL; + bat->buf = NULL; + bat->local = false; + bat->buffer_size = 0; + bat->period_size = 0; + bat->roundtriplatency = false; +#ifdef HAVE_LIBTINYALSA + bat->channels = 2; + bat->playback.fct = &playback_tinyalsa; + bat->capture.fct = &record_tinyalsa; +#else + bat->channels = 1; + bat->playback.fct = &playback_alsa; + bat->capture.fct = &record_alsa; +#endif + bat->playback.mode = MODE_LOOPBACK; + bat->capture.mode = MODE_LOOPBACK; + bat->period_is_limited = false; + bat->log = stdout; + bat->err = stderr; +} + +static void parse_arguments(struct bat *bat, int argc, char *argv[]) +{ + int c, option_index, err; + static const char short_options[] = "D:P:C:f:n:F:c:r:s:k:p:B:E:lth"; + static const struct option long_options[] = { + {"help", 0, 0, 'h'}, + {"log", 1, 0, OPT_LOG}, + {"file", 1, 0, OPT_READFILE}, + {"saveplay", 1, 0, OPT_SAVEPLAY}, + {"local", 0, 0, OPT_LOCAL}, + {"standalone", 0, 0, OPT_STANDALONE}, + {"roundtriplatency", 0, 0, OPT_ROUNDTRIPLATENCY}, + {"snr-db", 1, 0, OPT_SNRTHD_DB}, + {"snr-pc", 1, 0, OPT_SNRTHD_PC}, + {"readcapture", 1, 0, OPT_READCAPTURE}, + {0, 0, 0, 0} + }; + + while ((c = getopt_long(argc, argv, short_options, long_options, + &option_index)) != -1) { + switch (c) { + case OPT_LOG: + bat->logarg = optarg; + break; + case OPT_READFILE: + bat->playback.file = optarg; + break; + case OPT_SAVEPLAY: + bat->debugplay = optarg; + break; + case OPT_READCAPTURE: + bat->capturefile = optarg; + bat->capture.mode = MODE_ANALYZE_ONLY; + bat->playback.mode = MODE_ANALYZE_ONLY; + break; + case OPT_LOCAL: + bat->local = true; + break; + case OPT_STANDALONE: + bat->standalone = true; + break; + case OPT_ROUNDTRIPLATENCY: + bat->roundtriplatency = true; + break; + case OPT_SNRTHD_DB: + get_snr_thd_db(bat, optarg); + break; + case OPT_SNRTHD_PC: + get_snr_thd_pc(bat, optarg); + break; + case 'D': + if (bat->playback.device == NULL) + bat->playback.device = optarg; + if (bat->capture.device == NULL) + bat->capture.device = optarg; + break; + case 'P': + if (bat->capture.mode == MODE_SINGLE) + bat->capture.mode = MODE_LOOPBACK; + else + bat->playback.mode = MODE_SINGLE; + bat->playback.device = optarg; + break; + case 'C': + if (bat->playback.mode == MODE_SINGLE) + bat->playback.mode = MODE_LOOPBACK; + else + bat->capture.mode = MODE_SINGLE; + bat->capture.device = optarg; + break; + case 'n': + bat->narg = optarg; + break; + case 'F': + get_sine_frequencies(bat, optarg); + break; + case 'c': + bat->channels = atoi(optarg); + break; + case 'r': + bat->rate = atoi(optarg); + break; + case 'f': + get_format(bat, optarg); + break; + case 'k': + bat->sigma_k = atof(optarg); + break; + case 'p': + bat->periods_total = atoi(optarg); + bat->period_is_limited = true; + break; + case 'B': + err = atoi(optarg); + bat->buffer_size = err >= MIN_BUFFERSIZE + && err < MAX_BUFFERSIZE ? err : 0; + break; + case 'E': + err = atoi(optarg); + bat->period_size = err >= MIN_PERIODSIZE + && err < MAX_PERIODSIZE ? err : 0; + break; + case 'h': + default: + usage(bat); + exit(EXIT_SUCCESS); + } + } +} + +static int validate_options(struct bat *bat) +{ + int c; + float freq_low, freq_high; + + /* check if we have an input file for local mode */ + if ((bat->local == true) && (bat->capture.file == NULL)) { + fprintf(bat->err, _("no input file for local testing\n")); + return -EINVAL; + } + + /* check supported channels */ + if (bat->channels > MAX_CHANNELS || bat->channels < MIN_CHANNELS) { + fprintf(bat->err, _("%d channels not supported\n"), + bat->channels); + return -EINVAL; + } + + /* check single ended is in either playback or capture - not both */ + if ((bat->playback.mode == MODE_SINGLE) + && (bat->capture.mode == MODE_SINGLE)) { + fprintf(bat->err, _("single ended mode is simplex\n")); + return -EINVAL; + } + + /* check sine wave frequency range */ + freq_low = DC_THRESHOLD; + freq_high = bat->rate * RATE_FACTOR; + for (c = 0; c < bat->channels; c++) { + if (bat->target_freq[c] < freq_low + || bat->target_freq[c] > freq_high) { + fprintf(bat->err, _("sine wave frequency out of")); + fprintf(bat->err, _(" range: (%.1f, %.1f)\n"), + freq_low, freq_high); + return -EINVAL; + } + } + + return 0; +} + +static int bat_init(struct bat *bat) +{ + int err = 0; + int fd = 0; + char name[] = TEMP_RECORD_FILE_NAME; + + /* Determine logging to a file or stdout and stderr */ + if (bat->logarg) { + bat->log = NULL; + bat->log = fopen(bat->logarg, "wb"); + if (bat->log == NULL) { + err = -errno; + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->logarg, err); + return err; + } + bat->err = bat->log; + } + + /* Determine duration of playback and/or capture */ + if (bat->narg) { + err = get_duration(bat); + if (err < 0) + return err; + } + + /* Set default playback and capture devices */ + if (bat->playback.device == NULL && bat->capture.device == NULL) + bat->playback.device = bat->capture.device = DEFAULT_DEV_NAME; + + /* Determine capture file */ + if (bat->local) { + bat->capture.file = bat->playback.file; + } else { + /* create temp file for sound record and analysis */ + fd = mkstemp(name); + if (fd == -1) { + err = -errno; + fprintf(bat->err, _("Fail to create record file: %d\n"), + err); + return err; + } + /* store file name which is dynamically created */ + bat->capture.file = strdup(name); + if (bat->capture.file == NULL) + return -ENOMEM; + /* close temp file */ + close(fd); + } + + /* Initial for playback */ + if (bat->playback.file == NULL) { + /* No input file so we will generate our own sine wave */ + if (bat->frames) { + if (bat->playback.mode == MODE_SINGLE) { + /* Play nb of frames given by -n argument */ + bat->sinus_duration = bat->frames; + } else { + /* Play CAPTURE_DELAY msec + + * 150% of the nb of frames to be analyzed */ + bat->sinus_duration = bat->rate * + CAPTURE_DELAY / 1000; + bat->sinus_duration += + (bat->frames + bat->frames / 2); + } + } else { + /* Special case where we want to generate a sine wave + * endlessly without capturing */ + bat->sinus_duration = 0; + bat->playback.mode = MODE_SINGLE; + } + } else { + bat->fp = fopen(bat->playback.file, "rb"); + if (bat->fp == NULL) { + err = -errno; + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->playback.file, err); + return err; + } + err = read_wav_header(bat, bat->playback.file, bat->fp, false); + fclose(bat->fp); + if (err != 0) + return err; + } + + bat->frame_size = bat->sample_size * bat->channels; + + /* Set conversion functions */ + switch (bat->sample_size) { + case 1: + bat->convert_float_to_sample = convert_float_to_uint8; + bat->convert_sample_to_float = convert_uint8_to_float; + break; + case 2: + bat->convert_float_to_sample = convert_float_to_int16; + bat->convert_sample_to_float = convert_int16_to_float; + break; + case 3: + bat->convert_float_to_sample = convert_float_to_int24; + bat->convert_sample_to_float = convert_int24_to_float; + break; + case 4: + bat->convert_float_to_sample = convert_float_to_int32; + bat->convert_sample_to_float = convert_int32_to_float; + break; + default: + fprintf(bat->err, _("Invalid PCM format: size=%d\n"), + bat->sample_size); + return -EINVAL; + } + + return err; +} + +int main(int argc, char *argv[]) +{ + struct bat bat; + int err = 0; + + set_defaults(&bat); + +#ifdef ENABLE_NLS + setlocale(LC_ALL, ""); + textdomain(PACKAGE); +#endif + + fprintf(bat.log, _("%s version %s\n\n"), PACKAGE_NAME, PACKAGE_VERSION); + + parse_arguments(&bat, argc, argv); + + err = bat_init(&bat); + if (err < 0) + goto out; + + err = validate_options(&bat); + if (err < 0) + goto out; + + /* round trip latency test thread */ + if (bat.roundtriplatency) { + while (1) { + fprintf(bat.log, + _("\nStart round trip latency\n")); + roundtrip_latency_init(&bat); + test_loopback(&bat); + + if (bat.latency.xrun_error == false) + break; + else { + /* Xrun error in playback or capture, + increase period size and try again */ + bat.period_size += bat.rate / 1000; + bat.buffer_size = + bat.period_size * DIV_BUFFERSIZE; + + /* terminate the test if period_size is + large enough */ + if (bat.period_size > bat.rate * 0.2) + break; + } + + /* Waiting 500ms and start the next round */ + usleep(CAPTURE_DELAY * 1000); + } + goto out; + } + + /* single line playback thread: playback only, no capture */ + if (bat.playback.mode == MODE_SINGLE) { + test_playback(&bat); + goto out; + } + + /* single line capture thread: capture only, no playback */ + if (bat.capture.mode == MODE_SINGLE) { + test_capture(&bat); + goto analyze; + } + + if (bat.capture.mode == MODE_ANALYZE_ONLY && bat.capturefile) { + bat.capture.file = strdup(bat.capturefile); + fprintf(bat.log, + _("Using data from file %s for analysis\n"), + bat.capture.file); + fprintf(bat.log, _("Skipping playback and capture\n")); + goto analyze; + } + + /* loopback thread: playback and capture in a loop */ + if (bat.local == false) + test_loopback(&bat); + +analyze: +#ifdef HAVE_LIBFFTW3F + if (!bat.standalone || snr_is_valid(bat.snr_thd_db)) + err = analyze_capture(&bat); +#else + fprintf(bat.log, _("No libfftw3 library. Exit without analysis.\n")); +#endif +out: + fprintf(bat.log, _("\nReturn value is %d\n"), err); + + if (bat.logarg) + fclose(bat.log); + if (!bat.local) + free(bat.capture.file); + + return err; +} diff --git a/bat/common.c b/bat/common.c new file mode 100644 index 0000000..470a7e6 --- /dev/null +++ b/bat/common.c @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include + +#include "gettext.h" + +#include "common.h" +#include "alsa.h" +#include "bat-signal.h" + +int retval_play; +int retval_record; + +/* update chunk_fmt data to bat */ +static int update_fmt_to_bat(struct bat *bat, struct chunk_fmt *fmt) +{ + bat->channels = fmt->channels; + bat->rate = fmt->sample_rate; + bat->sample_size = fmt->sample_length / 8; + if (bat->sample_size > 4) { + fprintf(bat->err, _("Invalid format: sample size=%d\n"), + bat->sample_size); + return -EINVAL; + } + bat->frame_size = fmt->blocks_align; + + return 0; +} + +/* calculate frames and update to bat */ +static int update_frames_to_bat(struct bat *bat, struct wav_chunk_header *header, + FILE *file ATTRIBUTE_UNUSED) +{ + /* The number of analyzed captured frames is arbitrarily set to half of + the number of frames of the wav file or the number of frames of the + wav file when doing direct analysis (--local) */ + bat->frames = header->length / bat->frame_size; + if (!bat->local) + bat->frames /= 2; + + return 0; +} + +static int read_chunk_fmt(struct bat *bat, char *file, FILE *fp, bool skip, + struct wav_chunk_header *header) +{ + size_t err; + int ret, header_skip; + struct chunk_fmt chunk_fmt; + + err = fread(&chunk_fmt, sizeof(chunk_fmt), 1, fp); + if (err != 1) { + fprintf(bat->err, _("Read chunk fmt error: %s:%zd\n"), + file, err); + return -EIO; + } + /* If the format header is larger, skip the rest */ + header_skip = header->length - sizeof(chunk_fmt); + if (header_skip > 0) { + ret = fseek(fp, header_skip, SEEK_CUR); + if (ret == -1) { + fprintf(bat->err, _("Seek fmt header error: %s:%d\n"), + file, ret); + return -EINVAL; + } + } + /* If the file is opened for playback, update BAT data; + If the file is opened for analysis, no update */ + if (skip == false) { + err = update_fmt_to_bat(bat, &chunk_fmt); + if (err != 0) + return err; + } + + return 0; +} + +int read_wav_header(struct bat *bat, char *file, FILE *fp, bool skip) +{ + struct wav_header riff_wave_header; + struct wav_chunk_header chunk_header; + int more_chunks = 1; + size_t err; + int ret; + + /* Read header of RIFF wav file */ + err = fread(&riff_wave_header, sizeof(riff_wave_header), 1, fp); + if (err != 1) { + fprintf(bat->err, _("Read header error: %s:%zd\n"), file, err); + return -EIO; + } + if ((riff_wave_header.magic != WAV_RIFF) + || (riff_wave_header.type != WAV_WAVE)) { + fprintf(bat->err, _("%s is not a riff/wave file\n"), file); + return -EINVAL; + } + + /* Read chunks in RIFF wav file */ + do { + err = fread(&chunk_header, sizeof(chunk_header), 1, fp); + if (err != 1) { + fprintf(bat->err, _("Read chunk header error: ")); + fprintf(bat->err, _("%s:%zd\n"), file, err); + return -EIO; + } + + switch (chunk_header.type) { + case WAV_FMT: + /* WAV_FMT chunk, read and analyze */ + err = read_chunk_fmt(bat, file, fp, skip, + &chunk_header); + if (err != 0) + return err; + break; + case WAV_DATA: + /* WAV_DATA chunk, break looping */ + /* If the file is opened for playback, update BAT data; + If the file is opened for analysis, no update */ + if (skip == false) { + err = update_frames_to_bat(bat, &chunk_header, + fp); + if (err != 0) + return err; + } + /* Stop looking for chunks */ + more_chunks = 0; + break; + default: + /* Unknown chunk, skip bytes */ + ret = fseek(fp, chunk_header.length, SEEK_CUR); + if (ret == -1) { + fprintf(bat->err, _("Fail to skip unknown")); + fprintf(bat->err, _(" chunk of %s:%d\n"), + file, ret); + return -EINVAL; + } + } + } while (more_chunks); + + return 0; +} + +void prepare_wav_info(struct wav_container *wav, struct bat *bat) +{ + wav->header.magic = WAV_RIFF; + wav->header.type = WAV_WAVE; + wav->format.magic = WAV_FMT; + wav->format.fmt_size = 16; + wav->format.format = WAV_FORMAT_PCM; + wav->format.channels = bat->channels; + wav->format.sample_rate = bat->rate; + wav->format.sample_length = bat->sample_size * 8; + wav->format.blocks_align = bat->channels * bat->sample_size; + wav->format.bytes_p_second = wav->format.blocks_align * bat->rate; + wav->chunk.length = bat->frames * bat->frame_size; + wav->chunk.type = WAV_DATA; + wav->header.length = (wav->chunk.length) + sizeof(wav->chunk) + + sizeof(wav->format) + sizeof(wav->header) - 8; +} + +int write_wav_header(FILE *fp, struct wav_container *wav, struct bat *bat) +{ + int err = 0; + + err = fwrite(&wav->header, 1, sizeof(wav->header), fp); + if (err != sizeof(wav->header)) { + fprintf(bat->err, _("Write file error: header %d\n"), err); + return -EIO; + } + err = fwrite(&wav->format, 1, sizeof(wav->format), fp); + if (err != sizeof(wav->format)) { + fprintf(bat->err, _("Write file error: format %d\n"), err); + return -EIO; + } + err = fwrite(&wav->chunk, 1, sizeof(wav->chunk), fp); + if (err != sizeof(wav->chunk)) { + fprintf(bat->err, _("Write file error: chunk %d\n"), err); + return -EIO; + } + + return 0; +} + +/* update wav header when data size changed */ +int update_wav_header(struct bat *bat, FILE *fp, int bytes) +{ + int err = 0; + struct wav_container wav; + + prepare_wav_info(&wav, bat); + wav.chunk.length = bytes; + wav.header.length = (wav.chunk.length) + sizeof(wav.chunk) + + sizeof(wav.format) + sizeof(wav.header) - 8; + rewind(fp); + err = write_wav_header(fp, &wav, bat); + + return err; +} + +/* + * Generate buffer to be played either from input file or from generated data + * Return value + * <0 error + * 0 ok + * >0 break + */ +int generate_input_data(struct bat *bat, void *buffer, int bytes, int frames) +{ + int err; + static int load; + + if (bat->playback.file != NULL) { + /* From input file */ + load = 0; + + while (1) { + err = fread((char *)buffer + load, 1, bytes - load, bat->fp); + if (0 == err) { + if (feof(bat->fp)) { + fprintf(bat->log, + _("End of playing.\n")); + return 1; + } + } else if (err < bytes - load) { + if (ferror(bat->fp)) { + fprintf(bat->err, _("Read file error")); + fprintf(bat->err, _(": %d\n"), err); + return -EIO; + } + load += err; + } else { + break; + } + } + } else { + /* Generate sine wave */ + if ((bat->sinus_duration) && (load > bat->sinus_duration)) + return 1; + + err = generate_sine_wave(bat, frames, buffer); + if (err != 0) + return err; + + load += frames; + } + + return 0; +} diff --git a/bat/common.h b/bat/common.h new file mode 100644 index 0000000..bb51b0d --- /dev/null +++ b/bat/common.h @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#ifndef ATTRIBUTE_UNUSED +/** do not print warning (gcc) when function parameter is not used */ +#define ATTRIBUTE_UNUSED __attribute__ ((__unused__)) +#endif + +#define TEMP_RECORD_FILE_NAME "/tmp/bat.wav.XXXXXX" +#define DEFAULT_DEV_NAME "default" + +#define OPT_BASE 300 +#define OPT_LOG (OPT_BASE + 1) +#define OPT_READFILE (OPT_BASE + 2) +#define OPT_SAVEPLAY (OPT_BASE + 3) +#define OPT_LOCAL (OPT_BASE + 4) +#define OPT_STANDALONE (OPT_BASE + 5) +#define OPT_ROUNDTRIPLATENCY (OPT_BASE + 6) +#define OPT_SNRTHD_DB (OPT_BASE + 7) +#define OPT_SNRTHD_PC (OPT_BASE + 8) +#define OPT_READCAPTURE (OPT_BASE + 9) + +#define COMPOSE(a, b, c, d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24)) +#define WAV_RIFF COMPOSE('R', 'I', 'F', 'F') +#define WAV_WAVE COMPOSE('W', 'A', 'V', 'E') +#define WAV_FMT COMPOSE('f', 'm', 't', ' ') +#define WAV_DATA COMPOSE('d', 'a', 't', 'a') +#define WAV_FORMAT_PCM 1 /* PCM WAVE file encoding */ + +#define MAX_CHANNELS 2 +#define MIN_CHANNELS 1 +#define MAX_PEAKS 10 +#define MAX_FRAMES (10 * 1024 * 1024) +/* Given in ms */ +#define CAPTURE_DELAY 500 +/* signal frequency should be less than samplerate * RATE_FACTOR */ +#define RATE_FACTOR 0.4 +/* valid range of samplerate: (1 - RATE_RANGE, 1 + RATE_RANGE) * samplerate */ +#define RATE_RANGE 0.05 +/* Given in us */ +#define MAX_BUFFERTIME 500000 +/* devide factor, was 4, changed to 8 to remove reduce capture overrun */ +#define DIV_BUFFERTIME 8 +/* margin to avoid sign inversion when generate sine wav */ +#define RANGE_FACTOR 0.95 +#define MAX_BUFFERSIZE 200000 +#define MIN_BUFFERSIZE 32 +#define MAX_PERIODSIZE 200000 +#define MIN_PERIODSIZE 32 +/* default period size for tinyalsa */ +#define TINYALSA_PERIODSIZE 1024 + +#define LATENCY_TEST_NUMBER 5 +#define LATENCY_TEST_TIME_LIMIT 25 +#define DIV_BUFFERSIZE 2 + +#define EBATBASE 1000 +#define ENOPEAK (EBATBASE + 1) +#define EONLYDC (EBATBASE + 2) +#define EBADPEAK (EBATBASE + 3) + +#define DC_THRESHOLD 7.01 + +/* tolerance of detected peak = max (DELTA_HZ, DELTA_RATE * target_freq). + * If DELTA_RATE is too high, BAT may not be able to recognize negative result; + * if too low, BAT may be too sensitive and results in uncecessary failure. */ +#define DELTA_RATE 0.005 +#define DELTA_HZ 1 + +#define FOUND_DC (1<<1) +#define FOUND_WRONG_PEAK (1<<0) + +/* Truncate sample frames to (1 << N), for faster FFT analysis process. The + * valid range of N is (SHIFT_MIN, SHIFT_MAX). When N increases, the analysis + * will be more time-consuming, and the result will be more accurate. */ +#define SHIFT_MAX (sizeof(int) * 8 - 2) +#define SHIFT_MIN 8 + +/* Define SNR range in dB. + * if the noise is equal to signal, SNR = 0.0dB; + * if the noise is zero, SNR is limited by RIFF wav data width: + * 8 bit --> 20.0 * log10f (powf(2.0, 8.0)) = 48.16 dB + * 16 bit --> 20.0 * log10f (powf(2.0, 16.0)) = 96.33 dB + * 24 bit --> 20.0 * log10f (powf(2.0, 24.0)) = 144.49 dB + * 32 bit --> 20.0 * log10f (powf(2.0, 32.0)) = 192.66 dB + * so define the SNR range (0.0, 200.0) dB, value out of range is invalid. */ +#define SNR_DB_INVALID -1.0 +#define SNR_DB_MIN 0.0 +#define SNR_DB_MAX 200.0 + +static inline bool snr_is_valid(float db) +{ + return (db > SNR_DB_MIN && db < SNR_DB_MAX); +} + +struct wav_header { + unsigned int magic; /* 'RIFF' */ + unsigned int length; /* file len */ + unsigned int type; /* 'WAVE' */ +}; + +struct wav_chunk_header { + unsigned int type; /* 'data' */ + unsigned int length; /* sample count */ +}; + +struct wav_fmt { + unsigned int magic; /* 'FMT '*/ + unsigned int fmt_size; /* 16 or 18 */ + unsigned short format; /* see WAV_FMT_* */ + unsigned short channels; + unsigned int sample_rate; /* Frequency of sample */ + unsigned int bytes_p_second; + unsigned short blocks_align; /* sample size; 1 or 2 bytes */ + unsigned short sample_length; /* 8, 12 or 16 bit */ +}; + +struct chunk_fmt { + unsigned short format; /* see WAV_FMT_* */ + unsigned short channels; + unsigned int sample_rate; /* Frequency of sample */ + unsigned int bytes_p_second; + unsigned short blocks_align; /* sample size; 1 or 2 bytes */ + unsigned short sample_length; /* 8, 12 or 16 bit */ +}; + +struct wav_container { + struct wav_header header; + struct wav_fmt format; + struct wav_chunk_header chunk; +}; + +struct bat; + +enum _bat_pcm_format { + BAT_PCM_FORMAT_UNKNOWN = -1, + BAT_PCM_FORMAT_S16_LE = 0, + BAT_PCM_FORMAT_S32_LE, + BAT_PCM_FORMAT_U8, + BAT_PCM_FORMAT_S24_3LE, + BAT_PCM_FORMAT_MAX +}; + +enum _bat_op_mode { + MODE_UNKNOWN = -1, + MODE_SINGLE = 0, + MODE_LOOPBACK, + MODE_ANALYZE_ONLY, + MODE_LAST +}; + +enum latency_state { + LATENCY_STATE_COMPLETE_FAILURE = -1, + LATENCY_STATE_COMPLETE_SUCCESS = 0, + LATENCY_STATE_MEASURE_FOR_1_SECOND, + LATENCY_STATE_PLAY_AND_LISTEN, + LATENCY_STATE_WAITING, +}; + +struct pcm { + unsigned int card_tiny; + unsigned int device_tiny; + char *device; + char *file; + enum _bat_op_mode mode; + void *(*fct)(struct bat *); +}; + +struct sin_generator; + +struct sin_generator { + double state_real; + double state_imag; + double phasor_real; + double phasor_imag; + float frequency; + float sample_rate; + float magnitude; +}; + +struct roundtrip_latency { + int number; + enum latency_state state; + float result[LATENCY_TEST_NUMBER]; + int final_result; + int samples; + float sum; + int threshold; + int error; + bool is_capturing; + bool is_playing; + bool xrun_error; +}; + +struct noise_analyzer { + int nsamples; /* number of sample */ + float *source; /* single-tone to be analyzed */ + float *target; /* target single-tone as standard */ + float rms_tgt; /* rms of target single-tone */ + float snr_db; /* snr in dB */ +}; + +struct bat { + unsigned int rate; /* sampling rate */ + int channels; /* nb of channels */ + int frames; /* nb of frames */ + int frame_size; /* size of frame */ + int sample_size; /* size of sample */ + enum _bat_pcm_format format; /* PCM format */ + int buffer_size; /* buffer size in frames */ + int period_size; /* period size in frames */ + + float sigma_k; /* threshold for peak detection */ + float snr_thd_db; /* threshold for noise detection (dB) */ + float target_freq[MAX_CHANNELS]; + + int sinus_duration; /* number of frames for playback */ + char *narg; /* argument string of duration */ + char *logarg; /* path name of log file */ + char *debugplay; /* path name to store playback signal */ + char *capturefile; /* path name for previously saved recording */ + bool standalone; /* enable to bypass analysis */ + bool roundtriplatency; /* enable round trip latency */ + + struct pcm playback; + struct pcm capture; + struct roundtrip_latency latency; + + unsigned int periods_played; + unsigned int periods_total; + bool period_is_limited; + + FILE *fp; + + FILE *log; + FILE *err; + + void (*convert_sample_to_float)(void *, float *, int); + void (*convert_float_to_sample)(float *, void *, int, int); + + void *buf; /* PCM Buffer */ + + bool local; /* true for internal test */ +}; + +struct analyze { + void *buf; + float *in; + float *out; + float *mag; +}; + +void prepare_wav_info(struct wav_container *, struct bat *); +int read_wav_header(struct bat *, char *, FILE *, bool); +int write_wav_header(FILE *, struct wav_container *, struct bat *); +int update_wav_header(struct bat *, FILE *, int); +int generate_input_data(struct bat *, void *, int, int); diff --git a/bat/convert.c b/bat/convert.c new file mode 100644 index 0000000..c502ae5 --- /dev/null +++ b/bat/convert.c @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" +#include +#include +#include +#include + +void convert_uint8_to_float(void *buf, float *val, int samples) +{ + int i; + + for (i = 0; i < samples; i++) + val[i] = ((uint8_t *) buf)[i]; +} + +void convert_int16_to_float(void *buf, float *val, int samples) +{ + int i; + + for (i = 0; i < samples; i++) + val[i] = ((int16_t *) buf)[i]; +} + +void convert_int24_to_float(void *buf, float *val, int samples) +{ + int i; + int32_t tmp; + + for (i = 0; i < samples; i++) { + tmp = ((uint8_t *) buf)[i * 3 + 2] << 24; + tmp |= ((uint8_t *) buf)[i * 3 + 1] << 16; + tmp |= ((uint8_t *) buf)[i * 3] << 8; + tmp >>= 8; + val[i] = tmp; + } +} + +void convert_int32_to_float(void *buf, float *val, int samples) +{ + int i; + + for (i = 0; i < samples; i++) + val[i] = ((int32_t *) buf)[i]; +} + +void convert_float_to_uint8(float *val, void *buf, int samples, int channels) +{ + int i, c, idx; + + for (i = 0; i < samples; i++) { + for (c = 0; c < channels; c++) { + idx = i * channels + c; + ((uint8_t *) buf)[idx] = (uint8_t) val[idx]; + } + } +} + +void convert_float_to_int16(float *val, void *buf, int samples, int channels) +{ + int i, c, idx; + + for (i = 0; i < samples; i++) { + for (c = 0; c < channels; c++) { + idx = i * channels + c; + ((int16_t *) buf)[idx] = (int16_t) val[idx]; + } + } +} + +void convert_float_to_int24(float *val, void *buf, int samples, int channels) +{ + int i, c, idx_f, idx_i; + int32_t val_f_i; + + for (i = 0; i < samples; i++) { + for (c = 0; c < channels; c++) { + idx_f = i * channels + c; + idx_i = 3 * idx_f; + val_f_i = (int32_t) val[idx_f]; + ((int8_t *) buf)[idx_i + 0] = + (int8_t) (val_f_i & 0xff); + ((int8_t *) buf)[idx_i + 1] = + (int8_t) ((val_f_i >> 8) & 0xff); + ((int8_t *) buf)[idx_i + 2] = + (int8_t) ((val_f_i >> 16) & 0xff); + } + } +} + +void convert_float_to_int32(float *val, void *buf, int samples, int channels) +{ + int i, c, idx; + + for (i = 0; i < samples; i++) { + for (c = 0; c < channels; c++) { + idx = i * channels + c; + ((int32_t *) buf)[idx] = (int32_t) val[idx]; + } + } +} diff --git a/bat/convert.h b/bat/convert.h new file mode 100644 index 0000000..699c07a --- /dev/null +++ b/bat/convert.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +void convert_uint8_to_float(void *, float *, int); +void convert_int16_to_float(void *, float *, int); +void convert_int24_to_float(void *, float *, int); +void convert_int32_to_float(void *, float *, int); +void convert_float_to_uint8(float *, void *, int, int); +void convert_float_to_int16(float *, void *, int, int); +void convert_float_to_int24(float *, void *, int, int); +void convert_float_to_int32(float *, void *, int, int); diff --git a/bat/latencytest.c b/bat/latencytest.c new file mode 100644 index 0000000..7c9db61 --- /dev/null +++ b/bat/latencytest.c @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include + +#include "common.h" +#include "bat-signal.h" +#include "gettext.h" + +/* How one measurement step works: + - Listen and measure the average loudness of the environment for 1 second. + - Create a threshold value 16 decibels higher than the average loudness. + - Begin playing a ~1000 Hz sine wave and start counting the samples elapsed. + - Stop counting and playing if the input's loudness is higher than the + threshold, as the output wave is probably coming back. + - Calculate the round trip audio latency value in milliseconds. */ + +static float sumaudio(struct bat *bat, short int *buffer, int frames) +{ + float sum = 0; + int n = 0; + + while (frames) { + frames--; + + for (n = 0; n < bat->channels; n++) { + sum += abs(buffer[0]); + buffer++; + } + } + + sum = sum / bat->channels; + + return sum; +} + +static void play_and_listen(struct bat *bat, void *buffer, int frames) +{ + int averageinput; + int n = 0; + float sum = 0; + float max = 0; + float min = 100000.0f; + short int *input; + int num = bat->latency.number; + + averageinput = (int) (sumaudio(bat, buffer, frames) / frames); + + /* The signal is above threshold + So our sine wave comes back on the input */ + if (averageinput > bat->latency.threshold) { + input = buffer; + + /* Check the location when it became loud enough */ + while (n < frames) { + if (*input++ > bat->latency.threshold) + break; + *input += bat->channels; + n++; + } + + /* Now we get the total round trip latency*/ + bat->latency.samples += n; + + /* Expect at least 1 buffer of round trip latency. */ + if (bat->latency.samples > frames) { + bat->latency.result[num - 1] = + (float) bat->latency.samples * 1000 / bat->rate; + fprintf(bat->log, + _("Test%d, round trip latency %dms\n"), + num, + (int) bat->latency.result[num - 1]); + + for (n = 0; n < num; n++) { + if (bat->latency.result[n] > max) + max = bat->latency.result[n]; + if (bat->latency.result[n] < min) + min = bat->latency.result[n]; + sum += bat->latency.result[n]; + } + + /* The maximum is higher than the minimum's double */ + if (max / min > 2.0f) { + bat->latency.state = + LATENCY_STATE_COMPLETE_FAILURE; + bat->latency.is_capturing = false; + return; + + /* Final results */ + } else if (num == LATENCY_TEST_NUMBER) { + bat->latency.final_result = + (int) (sum / LATENCY_TEST_NUMBER); + fprintf(bat->log, + _("Final round trip latency: %dms\n"), + bat->latency.final_result); + + bat->latency.state = + LATENCY_STATE_COMPLETE_SUCCESS; + bat->latency.is_capturing = false; + return; + + /* Next step */ + } else + bat->latency.state = LATENCY_STATE_WAITING; + + bat->latency.number++; + + } else + /* Happens when an early noise comes in */ + bat->latency.state = LATENCY_STATE_WAITING; + + } else { + /* Still listening */ + bat->latency.samples += frames; + + /* Do not listen to more than a second + Maybe too much background noise */ + if ((unsigned int)bat->latency.samples > bat->rate) { + bat->latency.error++; + + if (bat->latency.error > LATENCY_TEST_NUMBER) { + fprintf(bat->err, + _("Could not detect signal.")); + fprintf(bat->err, + _("Too much background noise?\n")); + bat->latency.state = + LATENCY_STATE_COMPLETE_FAILURE; + bat->latency.is_capturing = false; + return; + } + + /* let's start over */ + bat->latency.state = LATENCY_STATE_WAITING; + } + } + + return; +} + +static void calculate_threshold(struct bat *bat) +{ + float average; + float reference; + + /* Calculate the average loudness of the environment and create + a threshold value 16 decibels higher than the average loudness */ + average = bat->latency.sum / bat->latency.samples / 32767.0f; + reference = 20.0f * log10f(average) + 16.0f; + bat->latency.threshold = (int) (powf(10.0f, reference / 20.0f) + * 32767.0f); +} + +void roundtrip_latency_init(struct bat *bat) +{ + bat->latency.number = 1; + bat->latency.state = LATENCY_STATE_MEASURE_FOR_1_SECOND; + bat->latency.final_result = 0; + bat->latency.samples = 0; + bat->latency.sum = 0; + bat->latency.threshold = 0; + bat->latency.is_capturing = false; + bat->latency.is_playing = false; + bat->latency.error = 0; + bat->latency.xrun_error = false; + bat->frames = LATENCY_TEST_TIME_LIMIT * bat->rate; + bat->periods_played = 0; +} + +int handleinput(struct bat *bat, void *buffer, int frames) +{ + switch (bat->latency.state) { + /* Measuring average loudness for 1 second */ + case LATENCY_STATE_MEASURE_FOR_1_SECOND: + bat->latency.sum += sumaudio(bat, buffer, frames); + bat->latency.samples += frames; + + /* 1 second elapsed */ + if ((unsigned int)bat->latency.samples >= bat->rate) { + calculate_threshold(bat); + bat->latency.state = LATENCY_STATE_PLAY_AND_LISTEN; + bat->latency.samples = 0; + bat->latency.sum = 0; + } + break; + + /* Playing sine wave and listening if it comes back */ + case LATENCY_STATE_PLAY_AND_LISTEN: + play_and_listen(bat, buffer, frames); + break; + + /* Waiting 1 second */ + case LATENCY_STATE_WAITING: + bat->latency.samples += frames; + + if ((unsigned int)bat->latency.samples > bat->rate) { + /* 1 second elapsed, start over */ + bat->latency.samples = 0; + bat->latency.state = LATENCY_STATE_MEASURE_FOR_1_SECOND; + } + break; + + default: + return 0; + } + + return 0; +} + +int handleoutput(struct bat *bat, void *buffer, int bytes, int frames) +{ + int err = 0; + + /* If capture completed, terminate the playback */ + if (bat->periods_played * frames > 2 * bat->rate + && bat->latency.is_capturing == false) + return bat->latency.state; + + if (bat->latency.state == LATENCY_STATE_PLAY_AND_LISTEN) + err = generate_sine_wave(bat, frames, buffer); + else + /* Output silence */ + memset(buffer, 0, bytes); + + return err; +} diff --git a/bat/latencytest.h b/bat/latencytest.h new file mode 100644 index 0000000..be5e661 --- /dev/null +++ b/bat/latencytest.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +void roundtrip_latency_init(struct bat *); +int handleinput(struct bat *, void *, int); +int handleoutput(struct bat *, void *, int, int); diff --git a/bat/signal.c b/bat/signal.c new file mode 100644 index 0000000..da5368c --- /dev/null +++ b/bat/signal.c @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2015 Caleb Crome + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +/* + * This is a general purpose sine wave generator that will stay stable + * for a long time, and with a little renormalization, could stay stay + * stable indefinitely + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gettext.h" +#include "common.h" +#include "signal.h" + +/* + * Initialize the sine wave generator. + * sin_generator: gets initialized by this call. + * frequency: the frequency for the sine wave. must be < 0.5*sample_rate + * sample_rate: the sample rate... + * returns 0 on success, -1 on error. + */ +int sin_generator_init(struct sin_generator *sg, float magnitude, + float frequency, float sample_rate) +{ + /* angular frequency: cycles/sec / (samp/sec) * rad/cycle = rad/samp */ + float w = frequency / sample_rate * 2 * M_PI; + if (frequency >= sample_rate / 2) + return -1; + sg->phasor_real = cos(w); + sg->phasor_imag = sin(w); + sg->magnitude = magnitude; + sg->state_real = 0.0; + sg->state_imag = magnitude; + sg->frequency = frequency; + sg->sample_rate = sample_rate; + return 0; +} + +/* + * Generates the next sample in the sine wave. + * should be much faster than calling a sin function + * if it's inlined and optimized. + * + * returns the next value. no possibility of error. + */ +float sin_generator_next_sample(struct sin_generator *sg) +{ + /* get shorthand to pointers */ + const double pr = sg->phasor_real; + const double pi = sg->phasor_imag; + const double sr = sg->state_real; + const double si = sg->state_imag; + /* step the phasor -- complex multiply */ + sg->state_real = sr * pr - si * pi; + sg->state_imag = sr * pi + pr * si; + /* return the input value so sine wave starts at exactly 0.0 */ + return (float)sr; +} + +/* fills a vector with a sine wave */ +void sin_generator_vfill(struct sin_generator *sg, float *buf, int n) +{ + int i; + for (i = 0; i < n; i++) + *buf++ = sin_generator_next_sample(sg); +} + +static int reorder(struct bat *bat, float *val, int frames) +{ + float *new_buf = NULL; + int i, c, bytes; + + bytes = frames * bat->channels * sizeof(float); + + new_buf = (float *) malloc(bytes); + if (new_buf == NULL) { + fprintf(bat->err, _("Not enough memory.\n")); + return -ENOMEM; + } + + memcpy(new_buf, val, bytes); + for (i = 0; i < frames; i++) + for (c = 0; c < bat->channels; c++) + val[i * bat->channels + c] = + new_buf[c * frames + i]; + free(new_buf); + + return 0; +} + +static int adjust_waveform(struct bat *bat, float *val, int frames, + int channels) +{ + int i, nsamples, max; + float factor, offset = 0.0; + + switch (bat->format) { + case BAT_PCM_FORMAT_U8: + max = INT8_MAX; + offset = max; /* shift for unsigned format */ + break; + case BAT_PCM_FORMAT_S16_LE: + max = INT16_MAX; + break; + case BAT_PCM_FORMAT_S24_3LE: + max = (1 << 23) - 1; + break; + case BAT_PCM_FORMAT_S32_LE: + max = INT32_MAX; + break; + default: + fprintf(bat->err, _("Invalid PCM format: %d\n"), bat->format); + return -EINVAL; + } + + factor = max * RANGE_FACTOR; + nsamples = channels * frames; + + for (i = 0; i < nsamples; i++) + val[i] = val[i] * factor + offset; + + return 0; +} + +int generate_sine_wave(struct bat *bat, int frames, void *buf) +{ + int err = 0; + int c, nsamples; + float *sinus_f = NULL; + static struct sin_generator sg[MAX_CHANNELS]; + + nsamples = bat->channels * frames; + sinus_f = (float *) malloc(nsamples * sizeof(float)); + if (sinus_f == NULL) { + fprintf(bat->err, _("Not enough memory.\n")); + return -ENOMEM; + } + + for (c = 0; c < bat->channels; c++) { + /* initialize static struct at the first time */ + if (sg[c].frequency != bat->target_freq[c]) + sin_generator_init(&sg[c], 1.0, bat->target_freq[c], + bat->rate); + /* fill buffer for each channel */ + sin_generator_vfill(&sg[c], sinus_f + c * frames, frames); + } + + /* reorder samples to interleaved mode */ + err = reorder(bat, sinus_f, frames); + if (err != 0) + goto exit; + + /* adjust amplitude and offset of waveform */ + err = adjust_waveform(bat, sinus_f, frames, bat->channels); + if (err != 0) + goto exit; + + bat->convert_float_to_sample(sinus_f, buf, frames, bat->channels); + +exit: + free(sinus_f); + + return err; +} + +/* generate single channel sine waveform without sample conversion */ +int generate_sine_wave_raw_mono(struct bat *bat, float *buf, + float freq, int nsamples) +{ + int err = 0; + struct sin_generator sg; + + err = sin_generator_init(&sg, 1.0, freq, bat->rate); + if (err < 0) + return err; + sin_generator_vfill(&sg, buf, nsamples); + + /* adjust amplitude and offset of waveform */ + err = adjust_waveform(bat, buf, nsamples, 1); + + return err; +} diff --git a/bat/tests/Makefile.am b/bat/tests/Makefile.am new file mode 100644 index 0000000..d20eb3c --- /dev/null +++ b/bat/tests/Makefile.am @@ -0,0 +1,11 @@ +SUBDIRS=asound_state +alsabat_script_files = analog_audio_playback_and_capture.sh \ + dp_audio_playback.sh \ + dp_audio_subdevice_number.sh \ + hdmi_audio_playback.sh \ + hdmi_audio_subdevice_number.sh \ + map_test_case \ + README + +EXTRA_DIST = \ + $(alsabat_script_files) diff --git a/bat/tests/README b/bat/tests/README new file mode 100644 index 0000000..de69686 --- /dev/null +++ b/bat/tests/README @@ -0,0 +1,32 @@ + + automated test scripts for linux audio driver + based on alsa-lib interface by using alsabat +=============================================================================== + +This package contains the test scripts for linux audio driver based on +alsa-lib interface by using alsabat. +It supports analog and display(HDMI/DP) audio test. +The package needs the alsa-utils, alsa-lib installed environment. + +alsabat_main.sh + - the main entrance test script, + it will call the other scripts to run the tests + (test result will save in the ./log/ folder) +analog_audio_playback_and_capture.sh + - analog audio test script (please to loopback the + analog audio output to analog audio input) +hdmi_audio_playback.sh + - hdmi audio test script (please to loopback the hdmi audio output + to analog audio input) +dp_audio_playback.sh + - dp audio test script (please to loopback the dp audio + output to analog audio input) +map_test_case + - to map the test suite/cases to a test script +asound_state/ + - some asound.state config reference files + based on different platforms + + Focus Luo + Wang,Jinliang + Zhang,Keqiao diff --git a/bat/tests/alsabat_main.sh b/bat/tests/alsabat_main.sh new file mode 100755 index 0000000..478ac98 --- /dev/null +++ b/bat/tests/alsabat_main.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +#/* +# * Copyright (C) 2013-2016 Intel Corporation +# * +# * This program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# */ +#set -x + +#alsabat test scripts path +export ABAT_TEST_PATH=`pwd` + +#alsabat test log file, path+filename +Day=`date +"%Y-%m-%d-%H-%M"` +Log_FileName="test_result-"${Day}".log" +export ABAT_TEST_LOG_FILE=${ABAT_TEST_PATH}/log/${Log_FileName} + +#terminal display colour setting +ESC_GREEN="\033[32m" +ESC_RED="\033[31m" +ESC_YELLOW="\033[33;1m" +ESC_OFF="\033[0m" + +#total/pass/fail test cases number +total_case_number=0 +suit_number=1 +pass_number=0 +fail_number=0 +# =========================== Public function ========================== + +function get_platform_info() +{ + #to get the audio card number + Card_Number=$(aplay -l | grep "HDMI 0" | cut -b 6) + cd /proc/asound/card$Card_Number/ + for file in `ls` + do + if [[ $file == codec* ]]; then + #to get the hardware platform ID, currently Intel skylake, + #broadwell and haswell hardware platforms are supported + Platform_ID=`cat $file |grep "Codec:" |cut -d " " -f 3` + if [ "$Platform_ID" == "Skylake" ] \ + || [ "$Platform_ID" == "Broadwell" ] \ + || [ "$Platform_ID" == "Haswell" ]; then + echo $Platform_ID + break + exit 0 + fi + else + printf '\033[1;31m %-30s %s \033[1;31m%s\n\033[0m' \ + "Get platform information failed"; + exit 1 + fi + done +} + +#printf the "pass" info in the file +show_pass() +{ + echo -e "$suit_number - [$1]:test ------- PASS" >> $ABAT_TEST_LOG_FILE + printf '\033[1;33m %-30s %s \033[1;32m%s\n\033[0m' \ +"$suit_number - [$1]" "-------------------------------- " "PASS"; +} + +#printf the "fail" info in the file +show_fail() +{ + echo -e "$suit_number - [$1]:test ------- FAIL" >> $ABAT_TEST_LOG_FILE + printf '\033[1;33m %-30s %s \033[1;31m%s\n\033[0m' \ +"$suit_number - [$1]" "-------------------------------- " "FAIL"; +} + + +function run_test() +{ + for TestItem in $@ + do + Date=`date` + Dot="$Dot". + echo "Now doing $TestItem test$Dot" + + #map test case to test script + eval item='$'$TestItem + + #to check the test script existing + if [ ! -f "$item" ]; then + echo -e "\e[31m not found $TestItem script,confirm it firstly" + echo -e "\e[0m" + exit 1 + fi + + #to run each test script + eval "\$$TestItem" + Result=$? + #record the test result to the log file + if [ $Result -eq 0 ]; then + show_pass "$TestItem" + else + show_fail "$TestItem" + fi + suit_number=$(($suit_number + 1)) + + done +} + +function test_suites ( ) +{ + #define the test suites/cases need to be run + TestProgram="verify_Analog_audio_playback_and_capture \ + verify_HDMI_audio_playback verify_DP_audio_playback" + + #run each test suites/test cases + run_test "$TestProgram" + + # to printf the detailed test results on the screen + cat $ABAT_TEST_LOG_FILE |grep FAIL + case_number=$(($case_number - 1)) + total_case_number=`cat $ABAT_TEST_LOG_FILE |grep -c "Test target frequency:"` + pass_number=`cat $ABAT_TEST_LOG_FILE |grep -c "Passed"` + fail_number=`cat $ABAT_TEST_LOG_FILE |grep -c "Failed"` + echo -e "\e[0m" + echo -e "\e[1;33m *---------------------------------------------------*\n" + echo -e " * "Total" ${total_case_number} "cases", \ +"PASS:" ${pass_number} "cases", "FAIL:" ${fail_number} "cases", \ +"Passrate is:" $((pass_number*100/total_case_number)) "%" *\n" + echo -e " *-------------------------------------------------------*\e[0m\n" + + #the the result also will be saved on the log file + echo "Total" ${total_case_number} "cases", \ +"PASS:" ${pass_number} "cases", "FAIL:" ${fail_number} "cases", \ +"Passrate:" $((pass_number*100/total_case_number)) "%" >> ${ABAT_TEST_LOG_FILE} + + #return 0, if the script finishs normally + exit 0 +} + +function main ( ) +{ + echo "Test results are as follows:" > ${ABAT_TEST_LOG_FILE} + get_platform_info # get hardware platform information + cd $ABAT_TEST_PATH + + # make sure the log folder is exist + if [ ! -d "$ABAT_TEST_PATH/log/" ]; then + mkdir "log" + fi + + #map the test cases to test scripts + source map_test_case + + #setting the alsa configure environment + alsactl restore -f $ABAT_TEST_PATH/asound_state/asound.state.$Platform_ID + + #Printf the user interface info + clear + echo -e "\e[1;33m" + date + echo -e "\e[0m" + echo -e "\e[1;33m *-------------------------------------------------*\n" + echo -e " *--Running the audio automated test on $Platform_ID-------*\n" + echo -e " *------------------------------------------------------*\e[0m\n" + read -p "Press enter to continue" + + #run the test suites/test cases + test_suites +} + +#the main entrance function +main diff --git a/bat/tests/analog_audio_playback_and_capture.sh b/bat/tests/analog_audio_playback_and_capture.sh new file mode 100755 index 0000000..bac491e --- /dev/null +++ b/bat/tests/analog_audio_playback_and_capture.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +#/* +# * Copyright (C) 2013-2016 Intel Corporation +# * +# * This program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# */ + +#set test freq table (HZ) +freq_table="10 31 73 155 380 977 1932 4119 8197 16197" + +#set test number of channels +test_channel=2 + +#get Analog audio card number +card_number=$(aplay -l | grep "Analog" | cut -b 6) +if [ "$card_number" = "" ]; then + echo "Can not get Analog card number." + exit 1 +fi + +#get Analog audio device number +device_number=$(aplay -l | grep "Analog"| cut -d " " -f 8 |cut -b 1) +if [ "$device_number" = "" ]; then + echo "Can not get Analog device number" + exit 1 +fi + + +device="hw:$card_number,$device_number" +echo $device + +#get Analog audio record card number +record_card_number=$(arecord -l | grep "Analog" | cut -b 6) +if [ "$record_card_number" = "" ]; then + echo "Can not get record card number." + exit 1 +fi + +#get Analog audio record device number +record_device_number=$(arecord -l | grep "Analog"| cut -d " " -f 8 |cut -b 1) +echo $record_device_number +if [ "$record_device_number" = "" ]; then + echo "Can not get record device number" + exit 1 +fi + +#Notice: to loopback the analog audio output to the analog audio input +record_device="hw:$record_card_number,$record_device_number" +test_flag=0 + +echo -e "\e[31m Notice: to loopback the analog audio output to \ +the analog audio input" +echo -e "\e[0m" +read -p "Press enter to continue" + +#call alsabat to do the test for each frequency in the freq_table +for freq in $freq_table + do + alsabat -P $device -C plug$record_device -c $test_channel -F $freq + if [ $? = 0 ]; then + echo "Test target frequency:$freq for Analog playback -- Passed \ +" >> $ABAT_TEST_LOG_FILE + echo "Test target frequency:$freq for Analog capture -- Passed \ +" >> $ABAT_TEST_LOG_FILE + else + echo "Test target frequency:$freq for Analog playback -- Failed \ +" >> $ABAT_TEST_LOG_FILE + echo "Test target frequency:$freq for Analog capture -- Failed \ +" >> $ABAT_TEST_LOG_FILE + test_flag=1 + fi + done + +exit $test_flag diff --git a/bat/tests/asound_state/Makefile.am b/bat/tests/asound_state/Makefile.am new file mode 100644 index 0000000..7010310 --- /dev/null +++ b/bat/tests/asound_state/Makefile.am @@ -0,0 +1,6 @@ +alsabat_cfg_files = asound.state.Broadwell \ + asound.state.Haswell \ + asound.state.Skylake + +EXTRA_DIST = \ + $(alsabat_cfg_files) diff --git a/bat/tests/asound_state/asound.state.Broadwell b/bat/tests/asound_state/asound.state.Broadwell new file mode 100644 index 0000000..bea75e5 --- /dev/null +++ b/bat/tests/asound_state/asound.state.Broadwell @@ -0,0 +1,439 @@ +state.HDMI { + control.1 { + iface CARD + name 'HDMI/DP,pcm=3 Jack' + value false + comment { + access read + type BOOLEAN + count 1 + } + } + control.2 { + iface MIXER + name 'IEC958 Playback Con Mask' + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.3 { + iface MIXER + name 'IEC958 Playback Pro Mask' + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.4 { + iface MIXER + name 'IEC958 Playback Default' + value '0482000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.5 { + iface MIXER + name 'IEC958 Playback Switch' + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.6 { + iface PCM + device 3 + name ELD + value '' + comment { + access 'read volatile' + type BYTES + count 0 + } + } + control.7 { + iface CARD + name 'HDMI/DP,pcm=7 Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.8 { + iface MIXER + name 'IEC958 Playback Con Mask' + index 1 + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.9 { + iface MIXER + name 'IEC958 Playback Pro Mask' + index 1 + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.10 { + iface MIXER + name 'IEC958 Playback Default' + index 1 + value '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.11 { + iface MIXER + name 'IEC958 Playback Switch' + index 1 + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.12 { + iface PCM + device 7 + name ELD + value '100008006a10000100000000000000000469b12341535553205041323338091707000000' + comment { + access 'read volatile' + type BYTES + count 36 + } + } + control.13 { + iface CARD + name 'HDMI/DP,pcm=8 Jack' + value false + comment { + access read + type BOOLEAN + count 1 + } + } + control.14 { + iface MIXER + name 'IEC958 Playback Con Mask' + index 2 + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.15 { + iface MIXER + name 'IEC958 Playback Pro Mask' + index 2 + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.16 { + iface MIXER + name 'IEC958 Playback Default' + index 2 + value '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.17 { + iface MIXER + name 'IEC958 Playback Switch' + index 2 + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.18 { + iface PCM + device 8 + name ELD + value '' + comment { + access 'read volatile' + type BYTES + count 0 + } + } + control.19 { + iface PCM + device 3 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } + control.20 { + iface PCM + device 7 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } + control.21 { + iface PCM + device 8 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } +} +state.PCH { + control.1 { + iface MIXER + name 'Master Playback Volume' + value.0 41 + value.1 41 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 87' + dbmin -6525 + dbmax 0 + dbvalue.0 -3450 + dbvalue.1 -3450 + } + } + control.2 { + iface MIXER + name 'Master Playback Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.3 { + iface MIXER + name 'Loopback Mixing' + value Enabled + comment { + access 'read write' + type ENUMERATED + count 1 + item.0 Disabled + item.1 Enabled + } + } + control.4 { + iface MIXER + name 'Mic Playback Volume' + value.0 0 + value.1 0 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 31' + dbmin -3450 + dbmax 1200 + dbvalue.0 -3450 + dbvalue.1 -3450 + } + } + control.5 { + iface MIXER + name 'Mic Playback Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.6 { + iface MIXER + name 'Capture Volume' + value.0 21 + value.1 21 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 63' + dbmin -1725 + dbmax 3000 + dbvalue.0 -150 + dbvalue.1 -150 + } + } + control.7 { + iface MIXER + name 'Capture Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.8 { + iface MIXER + name 'Mic Boost Volume' + value.0 0 + value.1 0 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 3' + dbmin 0 + dbmax 3600 + dbvalue.0 0 + dbvalue.1 0 + } + } + control.9 { + iface CARD + name 'Mic Jack' + value false + comment { + access read + type BOOLEAN + count 1 + } + } + control.10 { + iface CARD + name 'Headphone Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.11 { + iface PCM + name 'Playback Channel Map' + value.0 0 + value.1 0 + comment { + access read + type INTEGER + count 2 + range '0 - 36' + } + } + control.12 { + iface PCM + name 'Capture Channel Map' + value.0 0 + value.1 0 + comment { + access read + type INTEGER + count 2 + range '0 - 36' + } + } + control.13 { + iface MIXER + name 'PCM Playback Volume' + value.0 105 + value.1 105 + comment { + access 'read write user' + type INTEGER + count 2 + range '0 - 255' + tlv '0000000100000008ffffec1400000014' + dbmin -5100 + dbmax 0 + dbvalue.0 -3000 + dbvalue.1 -3000 + } + } + control.14 { + iface MIXER + name 'Digital Capture Volume' + value.0 60 + value.1 60 + comment { + access 'read write user' + type INTEGER + count 2 + range '0 - 120' + tlv '0000000100000008fffff44800000032' + dbmin -3000 + dbmax 3000 + dbvalue.0 0 + dbvalue.1 0 + } + } +} diff --git a/bat/tests/asound_state/asound.state.Haswell b/bat/tests/asound_state/asound.state.Haswell new file mode 100644 index 0000000..04b05c1 --- /dev/null +++ b/bat/tests/asound_state/asound.state.Haswell @@ -0,0 +1,342 @@ +state.HDMI { + control.1 { + iface CARD + name 'HDMI/DP,pcm=3 Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.2 { + iface MIXER + name 'IEC958 Playback Con Mask' + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.3 { + iface MIXER + name 'IEC958 Playback Pro Mask' + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.4 { + iface MIXER + name 'IEC958 Playback Default' + value '0482000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.5 { + iface MIXER + name 'IEC958 Playback Switch' + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.6 { + iface PCM + device 3 + name ELD + value '100008006a10000100000000000000000469d22341535553205653323339091707000000' + comment { + access 'read volatile' + type BYTES + count 36 + } + } + control.7 { + iface CARD + name 'HDMI/DP,pcm=7 Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.8 { + iface MIXER + name 'IEC958 Playback Con Mask' + index 1 + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.9 { + iface MIXER + name 'IEC958 Playback Pro Mask' + index 1 + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.10 { + iface MIXER + name 'IEC958 Playback Default' + index 1 + value '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.11 { + iface MIXER + name 'IEC958 Playback Switch' + index 1 + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.12 { + iface PCM + device 7 + name ELD + value '100008006a10000100000000000000000469b12341535553205041323338091707000000' + comment { + access 'read volatile' + type BYTES + count 36 + } + } + control.13 { + iface PCM + device 3 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } + control.14 { + iface PCM + device 7 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } +} +state.PCH { + control.1 { + iface MIXER + name 'Master Playback Volume' + value.0 45 + value.1 45 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 87' + dbmin -6525 + dbmax 0 + dbvalue.0 -3150 + dbvalue.1 -3150 + } + } + control.2 { + iface MIXER + name 'Master Playback Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.3 { + iface MIXER + name 'Capture Volume' + value.0 27 + value.1 27 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 63' + dbmin -1725 + dbmax 3000 + dbvalue.0 300 + dbvalue.1 300 + } + } + control.4 { + iface MIXER + name 'Capture Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.5 { + iface MIXER + name 'Mic Boost Volume' + value.0 0 + value.1 0 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 3' + dbmin 0 + dbmax 3600 + dbvalue.0 0 + dbvalue.1 0 + } + } + control.6 { + iface MIXER + name 'Internal Mic Boost Volume' + value.0 0 + value.1 0 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 3' + dbmin 0 + dbmax 3600 + dbvalue.0 0 + dbvalue.1 0 + } + } + control.7 { + iface CARD + name 'Mic Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.8 { + iface CARD + name 'Internal Mic Phantom Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.9 { + iface CARD + name 'Headphone Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.10 { + iface PCM + name 'Playback Channel Map' + value.0 0 + value.1 0 + comment { + access read + type INTEGER + count 2 + range '0 - 36' + } + } + control.11 { + iface PCM + name 'Capture Channel Map' + value.0 0 + value.1 0 + comment { + access read + type INTEGER + count 2 + range '0 - 36' + } + } + control.12 { + iface MIXER + name 'PCM Playback Volume' + value.0 167 + value.1 167 + comment { + access 'read write user' + type INTEGER + count 2 + range '0 - 255' + tlv '0000000100000008ffffec1400000014' + dbmin -5100 + dbmax 0 + dbvalue.0 -1760 + dbvalue.1 -1760 + } + } + control.13 { + iface MIXER + name 'Digital Capture Volume' + value.0 60 + value.1 60 + comment { + access 'read write user' + type INTEGER + count 2 + range '0 - 120' + tlv '0000000100000008fffff44800000032' + dbmin -3000 + dbmax 3000 + dbvalue.0 0 + dbvalue.1 0 + } + } +} diff --git a/bat/tests/asound_state/asound.state.Skylake b/bat/tests/asound_state/asound.state.Skylake new file mode 100644 index 0000000..50f672a --- /dev/null +++ b/bat/tests/asound_state/asound.state.Skylake @@ -0,0 +1,437 @@ +state.PCH { + control.1 { + iface MIXER + name 'Master Playback Volume' + value.0 57 + value.1 57 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 87' + dbmin -6525 + dbmax 0 + dbvalue.0 -2250 + dbvalue.1 -2250 + } + } + control.2 { + iface MIXER + name 'Master Playback Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.3 { + iface MIXER + name 'Loopback Mixing' + value Enabled + comment { + access 'read write' + type ENUMERATED + count 1 + item.0 Disabled + item.1 Enabled + } + } + control.4 { + iface MIXER + name 'Mic Playback Volume' + value.0 15 + value.1 15 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 31' + dbmin -3450 + dbmax 1200 + dbvalue.0 -1200 + dbvalue.1 -1200 + } + } + control.5 { + iface MIXER + name 'Mic Playback Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.6 { + iface MIXER + name 'Capture Volume' + value.0 27 + value.1 27 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 63' + dbmin -1725 + dbmax 3000 + dbvalue.0 300 + dbvalue.1 300 + } + } + control.7 { + iface MIXER + name 'Capture Switch' + value.0 true + value.1 true + comment { + access 'read write' + type BOOLEAN + count 2 + } + } + control.8 { + iface MIXER + name 'Mic Boost Volume' + value.0 0 + value.1 0 + comment { + access 'read write' + type INTEGER + count 2 + range '0 - 3' + dbmin 0 + dbmax 3600 + dbvalue.0 0 + dbvalue.1 0 + } + } + control.9 { + iface CARD + name 'Mic Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.10 { + iface CARD + name 'Headphone Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.11 { + iface PCM + name 'Playback Channel Map' + value.0 0 + value.1 0 + comment { + access read + type INTEGER + count 2 + range '0 - 36' + } + } + control.12 { + iface PCM + name 'Capture Channel Map' + value.0 0 + value.1 0 + comment { + access read + type INTEGER + count 2 + range '0 - 36' + } + } + control.13 { + iface CARD + name 'HDMI/DP,pcm=3 Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.14 { + iface MIXER + name 'IEC958 Playback Con Mask' + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.15 { + iface MIXER + name 'IEC958 Playback Pro Mask' + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.16 { + iface MIXER + name 'IEC958 Playback Default' + value '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.17 { + iface MIXER + name 'IEC958 Playback Switch' + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.18 { + iface PCM + device 3 + name ELD + value '100008006a10000100000000000000000469d22341535553205653323339091707000000' + comment { + access 'read volatile' + type BYTES + count 36 + } + } + control.19 { + iface CARD + name 'HDMI/DP,pcm=7 Jack' + value true + comment { + access read + type BOOLEAN + count 1 + } + } + control.20 { + iface MIXER + name 'IEC958 Playback Con Mask' + index 1 + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.21 { + iface MIXER + name 'IEC958 Playback Pro Mask' + index 1 + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.22 { + iface MIXER + name 'IEC958 Playback Default' + index 1 + value '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.23 { + iface MIXER + name 'IEC958 Playback Switch' + index 1 + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.24 { + iface PCM + device 7 + name ELD + value '100008006a14000100000000000000000469b12341535553205041323338091707000000' + comment { + access 'read volatile' + type BYTES + count 36 + } + } + control.25 { + iface CARD + name 'HDMI/DP,pcm=8 Jack' + value false + comment { + access read + type BOOLEAN + count 1 + } + } + control.26 { + iface MIXER + name 'IEC958 Playback Con Mask' + index 2 + value '0fff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.27 { + iface MIXER + name 'IEC958 Playback Pro Mask' + index 2 + value '0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access read + type IEC958 + count 1 + } + } + control.28 { + iface MIXER + name 'IEC958 Playback Default' + index 2 + value '0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + comment { + access 'read write' + type IEC958 + count 1 + } + } + control.29 { + iface MIXER + name 'IEC958 Playback Switch' + index 2 + value true + comment { + access 'read write' + type BOOLEAN + count 1 + } + } + control.30 { + iface PCM + device 8 + name ELD + value '' + comment { + access 'read volatile' + type BYTES + count 0 + } + } + control.31 { + iface PCM + device 3 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } + control.32 { + iface PCM + device 7 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } + control.33 { + iface PCM + device 8 + name 'Playback Channel Map' + value.0 0 + value.1 0 + value.2 0 + value.3 0 + value.4 0 + value.5 0 + value.6 0 + value.7 0 + comment { + access 'read write' + type INTEGER + count 8 + range '0 - 36' + } + } + control.34 { + iface MIXER + name 'PCM Playback Volume' + value.0 158 + value.1 158 + comment { + access 'read write user' + type INTEGER + count 2 + range '0 - 255' + tlv '0000000100000008ffffec1400000014' + dbmin -5100 + dbmax 0 + dbvalue.0 -1940 + dbvalue.1 -1940 + } + } + control.35 { + iface MIXER + name 'Digital Capture Volume' + value.0 60 + value.1 60 + comment { + access 'read write user' + type INTEGER + count 2 + range '0 - 120' + tlv '0000000100000008fffff44800000032' + dbmin -3000 + dbmax 3000 + dbvalue.0 0 + dbvalue.1 0 + } + } +} diff --git a/bat/tests/dp_audio_playback.sh b/bat/tests/dp_audio_playback.sh new file mode 100755 index 0000000..9c7ee7e --- /dev/null +++ b/bat/tests/dp_audio_playback.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +#/* +# * Copyright (C) 2013-2016 Intel Corporation +# * +# * This program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# */ + +#set test freq table (HZ) +freq_table="10 31 73 155 380 977 1932 4119 8197 16197" + +#set test number of channels +test_channel=2 + +#get device number for DP +DP_device_num=0 +$ABAT_TEST_PATH/dp_audio_subdevice_number.sh +DP_device_num=$? +if [ $DP_device_num = 77 ]; then + echo "Prompt: Can not get device with DP audio or \ +show the wrong connection type as HDMI in ELD info" + exit 1 +fi + +#To get DP audio device number +DP_card_number=$(aplay -l | grep "HDMI 0" | cut -b 6) +if [ "$DP_card_number" = "" ]; then + echo "Error: Can not get Display audio card." + exit 1 +fi + +DP_device="hw:$DP_card_number,$DP_device_num" +echo $device +sleep 2 + +#get Analog audio record card number +record_card_number=$(arecord -l | grep "Analog" | cut -b 6) +if [ "$record_card_number" = "" ]; then + echo "Can not get record card number." + exit 1 +fi + +#get Analog audio record device number +record_device_number=$(arecord -l | grep "Analog"| cut -d " " -f 8 |cut -b 1) +echo $record_device_number +if [ "$record_device_number" = "" ]; then + echo "Can not get record device number" + exit 1 +fi + +#Notice: to loopback the DP audio output to the analog audio input +record_device="hw:$record_card_number,$record_device_number" +test_flag=0 + +echo -e "\e[31m Notice: to loopback the DP audio \ +output to the analog audio input" +echo -e "\e[0m" +read -p "Press enter to continue" + +#call alsabat to do the test for each frequency in the freq_table +for freq in $freq_table + do + alsabat -P $DP_device -C plug$record_device -c $test_channel -F $freq + if [ $? = 0 ]; then + echo "Test target frequency:$freq for DP audio playback--Passed" \ +>> $ABAT_TEST_LOG_FILE + else + echo "Test target frequency:$freq for DP audio playback--Failed" \ +>> $ABAT_TEST_LOG_FILE + test_flag=1 + fi + done + +exit $test_flag diff --git a/bat/tests/dp_audio_subdevice_number.sh b/bat/tests/dp_audio_subdevice_number.sh new file mode 100755 index 0000000..db2e79e --- /dev/null +++ b/bat/tests/dp_audio_subdevice_number.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +#/* +# * Copyright (C) 2013-2016 Intel Corporation +# * +# * This program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# */ +#set -x + +subdevice_number=0 +get_subdevice=0 + +#make sure the DP monitor is connected and active + +# To get DisplayPort audio device number +card_number=$(aplay -l | grep "HDMI 1" | cut -b 6) +echo $card_number +if [ "$card_number" = "" ]; then + echo "Can not get Display audio card." + exit 254 +fi + +audio_card_dir="/proc/asound/card$card_number/" + +cd $audio_card_dir + +for file in `ls` +do + #To get the ELD info according to the connented monitor with DisplayPort. + if [[ $file == eld* ]]; then + let subdevice_number+=1 + cat $file | grep connection_type | grep DisplayPort > /dev/null + if [ $? = 0 ]; then + echo "Get the ELD information according to the connented \ +monitor with DisplayPort." + get_subdevice=1 + break + fi + fi +done + +#failed to get the subdevice number of DisplayPort audio +if [ $get_subdevice == 0 ]; then + exit 77 +fi + +#the subdevice number of DisplayPort audio is 3 +if [ $subdevice_number == 1 ]; then + exit 3 +#the subdevice number of DisplayPort audio is 7. +elif [ $subdevice_number == 2 ]; then + exit 7 +#the subdevice number of DisplayPort audio is 8 +elif [ $subdevice_number == 3 ]; then + exit 8 +#default: failed to get the subdevice number of DisplayPort audio +else + exit 77 +fi diff --git a/bat/tests/hdmi_audio_playback.sh b/bat/tests/hdmi_audio_playback.sh new file mode 100755 index 0000000..3c45d91 --- /dev/null +++ b/bat/tests/hdmi_audio_playback.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +#/* +# * Copyright (C) 2013-2016 Intel Corporation +# * +# * This program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# */ + +#set test freq table (HZ) +freq_table="10 31 73 155 380 977 1932 4119 8197 16197" + +#set test number of channels +test_channel=2 + +#get device number for HDMI +HDMI_device_num=0 +$ABAT_TEST_PATH/hdmi_audio_subdevice_number.sh +HDMI_device_num=$? +if [ $HDMI_device_num = 77 ]; then + echo "Prompt: Can not get device with HDMI audio or \ +show the wrong connection type as DP in ELD info" + exit 1 +fi + +#To get HDMI audio device number +HDMI_card_number=$(aplay -l | grep "HDMI 0" | cut -b 6) +if [ "$HDMI_card_number" = "" ]; then + echo "Error: Can not get Display audio card." + exit 1 +fi + +HDMI_device="hw:$HDMI_card_number,$HDMI_device_num" +echo $device +sleep 2 + +#get Analog audio record card number +record_card_number=$(arecord -l | grep "Analog" | cut -b 6) +if [ "$record_card_number" = "" ]; then + echo "Can not get record card number." + exit 1 +fi + +#get Analog audio record device number +record_device_number=$(arecord -l | grep "Analog"| cut -d " " -f 8 |cut -b 1) +if [ "$record_device_number" = "" ]; then + echo "Can not get record device number" + exit 1 +fi + +#Notice: to loopback the HDMI audio output to the analog audio input +record_device="hw:$record_card_number,$record_device_number" +test_flag=0 + +echo -e "\e[31m Notice: to loopback the HDMI audio output \ +to the analog audio input" +echo -e "\e[0m" +read -p "Press enter to continue" +#call alsabat to do the test for each frequency in the freq_table +for freq in $freq_table + do + alsabat -P $HDMI_device -C plug$record_device -c $test_channel -F $freq + if [ $? = 0 ]; then + echo "Test target frequency:$freq for HDMI audio playback \ +-- Passed " >> $ABAT_TEST_LOG_FILE + else + echo "Test target frequency:$freq for HDMI audio playback \ +-- Failed " >> $ABAT_TEST_LOG_FILE + test_flag=1 + fi + done + +exit $test_flag diff --git a/bat/tests/hdmi_audio_subdevice_number.sh b/bat/tests/hdmi_audio_subdevice_number.sh new file mode 100755 index 0000000..7811577 --- /dev/null +++ b/bat/tests/hdmi_audio_subdevice_number.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +#/* +# * Copyright (C) 2013-2016 Intel Corporation +# * +# * This program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# */ +#set -x + +subdevice_number=0 +get_subdevice=0 + +#make sure the HDMI monitor is connected and active ######## + +# To get HDMI audio device number +card_number=$(aplay -l | grep "HDMI 0" | cut -b 6) +if [ "$card_number" = "" ]; then + echo "Can not get Display audio card." + #failed to get Display audio card. + exit 1 +fi + +audio_card_dir="/proc/asound/card$card_number/" + +cd $audio_card_dir +for file in `ls` + do + #To get the ELD information according to the connented monitor with HDMI + if [[ $file == eld* ]]; then + let subdevice_number+=1 + cat $file | grep connection_type | grep HDMI > /dev/null + if [ $? = 0 ]; then + get_subdevice=1 + break + fi + fi + done + +#failed to get the subdevice number of HDMI audio. +if [ $get_subdevice == 0 ]; then + exit 77 +fi + +#the subdevice number of HDMI audio is 3. +if [ $subdevice_number == 1 ]; then + exit 3 +#the subdevice number of HDMI audio is 7. +elif [ $subdevice_number == 2 ]; then + exit 7 +#the subdevice number of HDMI audio is 8. +elif [ $subdevice_number == 3 ]; then + exit 8 +#default: failed to get the subdevice number of HDMI audio. +else + exit 77 +fi diff --git a/bat/tests/map_test_case b/bat/tests/map_test_case new file mode 100644 index 0000000..20eb223 --- /dev/null +++ b/bat/tests/map_test_case @@ -0,0 +1,8 @@ +# Analog audio basic test +verify_Analog_audio_playback_and_capture="$ABAT_TEST_PATH/analog_audio_playback_and_capture.sh" + +# Display audio basic test cases - for HDMI +verify_HDMI_audio_playback="$ABAT_TEST_PATH/hdmi_audio_playback.sh" + +# Display audio basic test cases - for DP +verify_DP_audio_playback="$ABAT_TEST_PATH/dp_audio_playback.sh" diff --git a/bat/tinyalsa.c b/bat/tinyalsa.c new file mode 100644 index 0000000..cde5709 --- /dev/null +++ b/bat/tinyalsa.c @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "gettext.h" + +#include "common.h" +#include "tinyalsa.h" +#include "latencytest.h" + +struct format_map_table { + enum _bat_pcm_format format_bat; + enum pcm_format format_tiny; +}; + +static struct format_map_table map_tables[] = { + { BAT_PCM_FORMAT_S16_LE, PCM_FORMAT_S16_LE }, + { BAT_PCM_FORMAT_S32_LE, PCM_FORMAT_S32_LE }, + { BAT_PCM_FORMAT_MAX, }, +}; + +static int format_convert(struct bat *bat, struct pcm_config *config) +{ + struct format_map_table *t = map_tables; + + for (; t->format_bat != BAT_PCM_FORMAT_MAX; t++) { + if (t->format_bat == bat->format) { + config->format = t->format_tiny; + return 0; + } + } + fprintf(bat->err, _("Invalid format!\n")); + return -EINVAL; +} + +static int init_config(struct bat *bat, struct pcm_config *config) +{ + config->channels = bat->channels; + config->rate = bat->rate; + if (bat->period_size > 0) + config->period_size = bat->period_size; + else + config->period_size = TINYALSA_PERIODSIZE; + config->period_count = 4; + config->start_threshold = 0; + config->stop_threshold = 0; + config->silence_threshold = 0; + + return format_convert(bat, config); +} + +/** + * Called when thread is finished + */ +static void close_handle(void *handle) +{ + struct pcm *pcm = handle; + + if (NULL != pcm) + pcm_close(pcm); +} + +/** + * Check that a parameter is inside bounds + */ +static int check_param(struct bat *bat, struct pcm_params *params, + unsigned int param, unsigned int value, + char *param_name, char *param_unit) +{ + unsigned int min; + unsigned int max; + int ret = 0; + + min = pcm_params_get_min(params, param); + if (value < min) { + fprintf(bat->err, + _("%s is %u%s, device only supports >= %u%s!\n"), + param_name, value, param_unit, min, param_unit); + ret = -EINVAL; + } + + max = pcm_params_get_max(params, param); + if (value > max) { + fprintf(bat->err, + _("%s is %u%s, device only supports <= %u%s!\n"), + param_name, value, param_unit, max, param_unit); + ret = -EINVAL; + } + + return ret; +} + +/** + * Check all parameters + */ +static int check_playback_params(struct bat *bat, + struct pcm_config *config) +{ + struct pcm_params *params; + unsigned int card = bat->playback.card_tiny; + unsigned int device = bat->playback.device_tiny; + int err = 0; + + params = pcm_params_get(card, device, PCM_OUT); + if (params == NULL) { + fprintf(bat->err, _("Unable to open PCM device %u!\n"), + device); + return -EINVAL; + } + + err = check_param(bat, params, PCM_PARAM_RATE, + config->rate, "Sample rate", "Hz"); + if (err < 0) + goto exit; + err = check_param(bat, params, PCM_PARAM_CHANNELS, + config->channels, "Sample", " channels"); + if (err < 0) + goto exit; + err = check_param(bat, params, PCM_PARAM_SAMPLE_BITS, + bat->sample_size * 8, "Bitrate", " bits"); + if (err < 0) + goto exit; + err = check_param(bat, params, PCM_PARAM_PERIOD_SIZE, + config->period_size, "Period size", "Hz"); + if (err < 0) + goto exit; + err = check_param(bat, params, PCM_PARAM_PERIODS, + config->period_count, "Period count", "Hz"); + if (err < 0) + goto exit; + +exit: + pcm_params_free(params); + + return err; +} + +/** + * Process output data for latency test + */ +static int latencytest_process_output(struct bat *bat, struct pcm *pcm, + void *buffer, int bytes) +{ + int err = 0; + int frames = bytes / bat->frame_size; + + fprintf(bat->log, _("Play sample with %d frames buffer\n"), frames); + + bat->latency.is_playing = true; + + while (1) { + /* generate output data */ + err = handleoutput(bat, buffer, bytes, frames); + if (err != 0) + break; + + err = pcm_write(pcm, buffer, bytes); + if (err != 0) + break; + + if (bat->latency.state == LATENCY_STATE_COMPLETE_SUCCESS) + break; + + bat->periods_played++; + } + + bat->latency.is_playing = false; + + return err; +} + +/** + * Play sample + */ +static int play_sample(struct bat *bat, struct pcm *pcm, + void *buffer, int bytes) +{ + int err = 0; + int frames = bytes / bat->frame_size; + FILE *fp = NULL; + int bytes_total = 0; + + if (bat->debugplay) { + fp = fopen(bat->debugplay, "wb"); + err = -errno; + if (fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->debugplay, err); + return err; + } + /* leave space for file header */ + if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) { + err = -errno; + fclose(fp); + return err; + } + } + + while (1) { + err = generate_input_data(bat, buffer, bytes, frames); + if (err != 0) + break; + + if (bat->debugplay) { + if (fwrite(buffer, 1, bytes, fp) != bytes) { + err = -EIO; + break; + } + bytes_total += bytes; + } + + bat->periods_played++; + if (bat->period_is_limited + && bat->periods_played >= bat->periods_total) + break; + + err = pcm_write(pcm, buffer, bytes); + if (err != 0) + break; + } + + if (bat->debugplay) { + update_wav_header(bat, fp, bytes_total); + fclose(fp); + } + return err; +} + +static int get_tiny_device(struct bat *bat, char *alsa_device, + unsigned int *tiny_card, unsigned int *tiny_device) +{ + char *tmp1, *tmp2, *tmp3; + + if (alsa_device == NULL) + goto fail; + + tmp1 = strchr(alsa_device, ':'); + if (tmp1 == NULL) + goto fail; + + tmp3 = tmp1 + 1; + tmp2 = strchr(tmp3, ','); + if (tmp2 == NULL) + goto fail; + + tmp1 = tmp2 + 1; + *tiny_device = atoi(tmp1); + *tmp2 = '\0'; + *tiny_card = atoi(tmp3); + *tmp2 = ','; + + return 0; +fail: + fprintf(bat->err, _("Invalid tiny device: %s\n"), alsa_device); + return -EINVAL; +} + +/** + * Play + */ +void *playback_tinyalsa(struct bat *bat) +{ + int err = 0; + struct pcm_config config; + struct pcm *pcm = NULL; + void *buffer = NULL; + int bufbytes; + + fprintf(bat->log, _("Entering playback thread (tinyalsa).\n")); + + retval_play = 0; + + /* init device */ + err = get_tiny_device(bat, bat->playback.device, + &bat->playback.card_tiny, + &bat->playback.device_tiny); + if (err < 0) { + retval_play = err; + goto exit1; + } + + /* init config */ + err = init_config(bat, &config); + if (err < 0) { + retval_play = err; + goto exit1; + } + + /* check param before open device */ + err = check_playback_params(bat, &config); + if (err < 0) { + retval_play = err; + goto exit1; + } + + /* open device */ + pcm = pcm_open(bat->playback.card_tiny, bat->playback.device_tiny, + PCM_OUT, &config); + if (!pcm || !pcm_is_ready(pcm)) { + fprintf(bat->err, _("Unable to open PCM device %u (%s)!\n"), + bat->playback.device_tiny, pcm_get_error(pcm)); + retval_play = -EINVAL; + goto exit1; + } + + /* init buffer */ + bufbytes = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm)); + buffer = malloc(bufbytes); + if (!buffer) { + retval_play = -ENOMEM; + goto exit2; + } + + /* init playback source */ + if (bat->playback.file == NULL) { + fprintf(bat->log, _("Playing generated audio sine wave")); + bat->sinus_duration == 0 ? + fprintf(bat->log, _(" endlessly\n")) : + fprintf(bat->log, _("\n")); + } else { + fprintf(bat->log, _("Playing input audio file: %s\n"), + bat->playback.file); + bat->fp = fopen(bat->playback.file, "rb"); + err = -errno; + if (bat->fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->playback.file, err); + retval_play = err; + goto exit3; + } + /* Skip header */ + err = read_wav_header(bat, bat->playback.file, bat->fp, true); + if (err != 0) { + retval_play = err; + goto exit4; + } + } + + if (bat->roundtriplatency) + err = latencytest_process_output(bat, pcm, buffer, bufbytes); + else + err = play_sample(bat, pcm, buffer, bufbytes); + if (err < 0) { + retval_play = err; + goto exit4; + } + +exit4: + if (bat->playback.file) + fclose(bat->fp); +exit3: + free(buffer); +exit2: + pcm_close(pcm); +exit1: + pthread_exit(&retval_play); +} + +/** + * Capture sample + */ +static int capture_sample(struct bat *bat, struct pcm *pcm, + void *buffer, unsigned int bytes) +{ + int err = 0; + FILE *fp = NULL; + unsigned int bytes_read = 0; + unsigned int bytes_count = bat->frames * bat->frame_size; + + remove(bat->capture.file); + fp = fopen(bat->capture.file, "wb"); + err = -errno; + if (fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->capture.file, err); + return err; + } + /* leave space for file header */ + if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) { + err = -errno; + fclose(fp); + return err; + } + + while (bytes_read < bytes_count && !pcm_read(pcm, buffer, bytes)) { + if (fwrite(buffer, 1, bytes, fp) != bytes) + break; + + bytes_read += bytes; + + bat->periods_played++; + + if (bat->period_is_limited + && bat->periods_played >= bat->periods_total) + break; + } + + err = update_wav_header(bat, fp, bytes_read); + + fclose(fp); + return err; +} + +/** + * Process input data for latency test + */ +static int latencytest_process_input(struct bat *bat, struct pcm *pcm, + void *buffer, unsigned int bytes) +{ + int err = 0; + FILE *fp = NULL; + unsigned int bytes_read = 0; + unsigned int bytes_count = bat->frames * bat->frame_size; + + remove(bat->capture.file); + fp = fopen(bat->capture.file, "wb"); + err = -errno; + if (fp == NULL) { + fprintf(bat->err, _("Cannot open file: %s %d\n"), + bat->capture.file, err); + return err; + } + /* leave space for file header */ + if (fseek(fp, sizeof(struct wav_container), SEEK_SET) != 0) { + err = -errno; + fclose(fp); + return err; + } + + bat->latency.is_capturing = true; + + while (bytes_read < bytes_count && !pcm_read(pcm, buffer, bytes)) { + if (fwrite(buffer, 1, bytes, fp) != bytes) + break; + + err = handleinput(bat, buffer, bytes / bat->frame_size); + if (err != 0) + break; + + if (bat->latency.is_playing == false) + break; + + bytes_read += bytes; + } + + bat->latency.is_capturing = false; + + err = update_wav_header(bat, fp, bytes_read); + + fclose(fp); + return err; +} + +/** + * Record + */ +void *record_tinyalsa(struct bat *bat) +{ + int err = 0; + struct pcm_config config; + struct pcm *pcm; + void *buffer; + unsigned int bufbytes; + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + + fprintf(bat->log, _("Entering capture thread (tinyalsa).\n")); + + retval_record = 0; + + /* init device */ + err = get_tiny_device(bat, bat->capture.device, + &bat->capture.card_tiny, + &bat->capture.device_tiny); + if (err < 0) { + retval_record = err; + goto exit1; + } + + /* init config */ + err = init_config(bat, &config); + if (err < 0) { + retval_record = err; + goto exit1; + } + + /* open device */ + pcm = pcm_open(bat->capture.card_tiny, bat->capture.device_tiny, + PCM_IN, &config); + if (!pcm || !pcm_is_ready(pcm)) { + fprintf(bat->err, _("Unable to open PCM device (%s)!\n"), + pcm_get_error(pcm)); + retval_record = -EINVAL; + goto exit1; + } + + /* init buffer */ + bufbytes = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm)); + buffer = malloc(bufbytes); + if (!buffer) { + retval_record = -ENOMEM; + goto exit2; + } + + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); + pthread_cleanup_push(close_handle, pcm); + pthread_cleanup_push(free, buffer); + + fprintf(bat->log, _("Recording ...\n")); + if (bat->roundtriplatency) + err = latencytest_process_input(bat, pcm, buffer, bufbytes); + else + err = capture_sample(bat, pcm, buffer, bufbytes); + if (err != 0) { + retval_record = err; + goto exit3; + } + + /* Normally we will never reach this part of code (unless error in + * previous call) (before exit3) as this thread will be cancelled + * by end of play thread. Except in single line mode. */ + pthread_cleanup_pop(0); + pthread_cleanup_pop(0); + pthread_exit(&retval_record); + +exit3: + free(buffer); +exit2: + pcm_close(pcm); +exit1: + pthread_exit(&retval_record); +} diff --git a/bat/tinyalsa.h b/bat/tinyalsa.h new file mode 100644 index 0000000..60f78f7 --- /dev/null +++ b/bat/tinyalsa.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2013-2015 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +extern int retval_play; +extern int retval_record; + +void *playback_tinyalsa(struct bat *); +void *record_tinyalsa(struct bat *); diff --git a/configure-android b/configure-android new file mode 100755 index 0000000..30dc1b2 --- /dev/null +++ b/configure-android @@ -0,0 +1,75 @@ +#!/bin/bash + +# +# Copyright (C) 2025 KonstaKANG +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +if [ ! -d "$ANDROID_NDK_HOME" ]; then + echo "ANDROID_NDK_HOME environment variable is not properly set!" + exit 1 +fi + +export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64 + +export TARGET=aarch64-linux-android +export API=35 +export CC="$TOOLCHAIN/bin/clang --target=$TARGET$API" +export CXX="$TOOLCHAIN/bin/clang++ --target=$TARGET$API" +export AR=$TOOLCHAIN/bin/llvm-ar +export AS=$CC +export LD=$TOOLCHAIN/bin/ld +export RANLIB=$TOOLCHAIN/bin/llvm-ranlib +export STRIP=$TOOLCHAIN/bin/llvm-strip + +echo "Running autoconf..." +aclocal -I ../alsa-lib/utils +cp Makefile.am Makefile.am.ok +cp configure.ac configure.ac.ok +gettextize -c -f --no-changelog +echo "EXTRA_DIST = gettext.m4" > m4/Makefile.am +cp Makefile.am.ok Makefile.am +cp configure.ac.ok configure.ac +touch ltconfig +libtoolize --force --copy --automake +aclocal -I ../alsa-lib/utils +autoheader +automake --foreign --copy --add-missing +touch depcomp +autoconf + +ALSA_CONFIG=" \ +--datadir=/vendor/usr/share \ +--disable-alsatest \ +--host=aarch64-linux" + +echo "Generating configuration..." +echo "./configure $ALSA_CONFIG $TARGET" +./configure $ALSA_CONFIG $TARGET + +cat > include/version.h <]) +AC_CHECK_HEADERS([alsa/mixer.h], [have_mixer="yes"], [have_mixer="no"], + [#include ]) +AC_CHECK_HEADERS([alsa/rawmidi.h], [have_rawmidi="yes"], [have_rawmidi="no"], + [#include ]) +AC_CHECK_HEADERS([alsa/seq.h], [have_seq="yes"], [have_seq="no"], + [#include ]) +AC_CHECK_HEADERS([alsa/use-case.h], [have_ucm="yes"], [have_ucm="no"], + [#include ]) +AC_CHECK_HEADERS([alsa/topology.h], [have_topology="yes"], [have_topology="no"], + [#include ]) +AC_CHECK_HEADERS([samplerate.h], [have_samplerate="yes"], [have_samplerate="no"], + [#include ]) + +AC_CHECK_LIB([asound], [snd_seq_client_info_get_card], [HAVE_SEQ_CLIENT_INFO_GET_CARD="yes"]) +AC_CHECK_LIB([atopology], [snd_tplg_save], [have_topology="yes"], [have_topology="no"]) + +# +# NOTE: The library 'libffado' (at least v2.4.1) executes ctor/dtor of instances +# for some objects in startup/finish routines of C runtime. As a result, it +# outputs some superfluos messages. Furthermore, it brings much memory leak +# internally. Totally, libffado support is not recommended at all in usual +# purposes except for technical preview. +# +AC_CHECK_LIB([ffado], [ffado_streaming_init], [have_ffado="yes"], [have_ffado="no"]) +AS_IF([test x"$have_ffado" = xyes], + [AC_DEFINE([WITH_FFADO], [1], [Define if FFADO library is available])]) + +# Test programs for axfer use shm by memfd_create(2). If not supported, open(2) is used alternatively. +AC_CHECK_FUNC([memfd_create], [have_memfd_create="yes"], [have_memfd_create="no"]) +AS_IF([test x$have_memfd_create = xyes], + [AC_DEFINE([HAVE_MEMFD_CREATE], [1], [Define if Linux kernel supports memfd_create system call])]) + +AM_CONDITIONAL(HAVE_PCM, test "$have_pcm" = "yes") +AM_CONDITIONAL(HAVE_MIXER, test "$have_mixer" = "yes") +AM_CONDITIONAL(HAVE_RAWMIDI, test "$have_rawmidi" = "yes") +AM_CONDITIONAL(HAVE_SEQ, test "$have_seq" = "yes") +AM_CONDITIONAL(HAVE_UCM, test "$have_ucm" = "yes") +AM_CONDITIONAL(HAVE_TOPOLOGY, test "$have_topology" = "yes" -a "$ac_cv_header_dlfcn_h" = "yes") +AM_CONDITIONAL(HAVE_SAMPLERATE, test "$have_samplerate" = "yes") +AM_CONDITIONAL(HAVE_FFADO, test "$have_ffado" = "yes") + +dnl Use tinyalsa +alsabat_backend_tiny= +AC_ARG_ENABLE(alsabat_backend_tiny, + AS_HELP_STRING([--enable-alsabat-backend-tiny], [Use tinyalsa for alsabat backend]), + [case "${enableval}" in + yes) alsabat_backend_tiny=true ;; + no) alsabat_backend_tiny=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-alsabat-backend-tiny) ;; + esac],[alsabat_backend_tiny=false]) + +dnl Disable bat +bat= +if test "$have_pcm" = "yes"; then +AC_ARG_ENABLE(bat, + AS_HELP_STRING([--disable-bat], [Disable bat compilation]), + [case "${enableval}" in + yes) bat=true ;; + no) bat=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-bat) ;; + esac],[bat=true]) +fi +AM_CONDITIONAL(BAT, test x$bat = xtrue) + +if test x$bat = xtrue; then + + saved_CFLAGS="$CFLAGS" + saved_LDFLAGS="$LDFLAGS" + saved_LIBS="$LIBS" + FFTW_INC="" + FFTW_LIB="" + FFTW_CFLAGS="" + dnl Check for libfftw3 + have_libfftw3="yes" + AC_CHECK_LIB([fftw3f], [fftwf_malloc], , [have_libfftw3="no"]) + dnl Check for libtinyalsa + have_libtinyalsa= + if test x$alsabat_backend_tiny = xtrue; then + have_libtinyalsa="yes" + AC_CHECK_LIB([tinyalsa], [pcm_open], , [have_libtinyalsa="no"]) + fi + AC_CHECK_LIB([m], [sqrtf], , [AC_MSG_ERROR([Error: Need sqrtf])]) + AC_CHECK_LIB([pthread], [pthread_create], , [AC_MSG_ERROR([Error: need PTHREAD library])]) + FFTW_CFLAGS="$CFLAGS" + FFTW_LIB="$LIBS" + CFLAGS="$saved_CFLAGS" + LDFLAGS="$saved_LDFLAGS" + LIBS="$saved_LIBS" + AC_SUBST(FFTW_INC) + AC_SUBST(FFTW_LIB) + AC_SUBST(FFTW_CFLAGS) + +fi +AM_CONDITIONAL(HAVE_LIBFFTW3, test "$have_libfftw3" = "yes") +AM_CONDITIONAL(HAVE_LIBTINYALSA, test "$have_libtinyalsa" = "yes") + +dnl Check for librt +LIBRT="" +AC_MSG_CHECKING(for librt) +AC_ARG_WITH(librt, + AS_HELP_STRING([--with-librt], [Use librt for monotonic clock (default = yes)]), + [ have_librt="$withval" ], [ have_librt="yes" ]) +if test "$have_librt" = "yes"; then + AC_CHECK_LIB([rt], [clock_gettime], [HAVE_LIBRT="yes"]) + if test "$HAVE_LIBRT" = "yes" ; then + LIBRT="-lrt" + AC_DEFINE([HAVE_LIBRT], 1, [Have librt]) + AC_DEFINE([HAVE_CLOCK_GETTIME], 1, [Have clock gettime]) + fi +else + AC_MSG_RESULT(no) +fi + +dnl Disable alsamixer +CURSESINC="" +CURSESLIB="" +CURSES_CFLAGS="" +alsamixer= +if test "$have_mixer" = "yes"; then +AC_ARG_ENABLE(alsamixer, + AS_HELP_STRING([--disable-alsamixer], [Disable alsamixer compilation]), + [case "${enableval}" in + yes) alsamixer=true ;; + no) alsamixer=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-alsamixer) ;; + esac],[alsamixer=true]) +fi +AM_CONDITIONAL(ALSAMIXER, test x$alsamixer = xtrue) + +dnl Disable alsaconf +AC_ARG_ENABLE(alsaconf, + AS_HELP_STRING([--disable-alsaconf], [Disable alsaconf packaging]), + [case "${enableval}" in + yes) alsaconf=true ;; + no) alsaconf=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-alsaconf) ;; + esac],[alsaconf=true]) +AM_CONDITIONAL(ALSACONF, test x$alsaconf = xtrue) + +dnl Disable alsaloop +AC_ARG_ENABLE(alsaloop, + AS_HELP_STRING([--disable-alsaloop], [Disable alsaloop packaging]), + [case "${enableval}" in + yes) alsaloop=true ;; + no) alsaloop=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-alsaloop) ;; + esac],[alsaloop=true]) +AM_CONDITIONAL(ALSALOOP, test x$alsaloop = xtrue) + +dnl Disable nhlt +AC_ARG_ENABLE(nhlt, + AS_HELP_STRING([--disable-nhlt], [Disable nhlt packaging]), + [case "${enableval}" in + yes) nhlt=true ;; + no) nhlt=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-nhlt) ;; + esac],[nhlt=true]) +AM_CONDITIONAL(NHLT, test x$nhlt = xtrue) + +xmlto_available="" +AC_ARG_ENABLE(xmlto, + AS_HELP_STRING([--disable-xmlto], [Disable man page creation via xmlto]), + xmlto="$enableval", xmlto="yes") +if test "$xmlto" = "yes"; then + AC_CHECK_PROG([xmlto_available], [xmlto], [yes]) +fi +AM_CONDITIONAL(USE_XMLTO, test x"$xmlto_available" = xyes) + +rst2man_available="" +AC_ARG_ENABLE(rst2man, + AS_HELP_STRING([--disable-rst2man], [Disable man page creation via rst2man]), + rst2man="$enableval", rst2man="yes") +if test "$rst2man" = "yes"; then + AC_CHECK_PROG([rst2man_available], [rst2man], [yes]) +fi +AM_CONDITIONAL(USE_RST2MAN, test x"$rst2man_available" = xyes) + +AC_ARG_WITH( + [udev-rules-dir], + AS_HELP_STRING([--with-udev-rules-dir=DIR],[Directory where to install udev rules to (default=auto)]), + [udevrulesdir="$withval"], + [udevdir=$($PKG_CONFIG udev --variable=udevdir) + if test "x$udevdir" = "x"; then + udevrulesdir="/lib/udev/rules.d" + else + udevrulesdir="$udevdir/rules.d" + fi]) +AC_SUBST(udevrulesdir) + +dnl Checks for header files. +AC_HEADER_STDC +if test x$alsamixer = xtrue; then + AC_ARG_WITH(curses, + AS_HELP_STRING([--with-curses=libname], [Specify the curses library to use (default=auto)]), + curseslib="$withval", + curseslib="auto") + CURSESLIBDIR="" + NCURSESLIBSUFFIX="" + CURSES_NLS="no" + if test "$curseslib" = "ncursesw" -o \( "$curseslib" = "auto" -a "$USE_NLS" = "yes" \); then + dnl First try out pkg-config, then fall back to old config scripts. + PKG_CHECK_MODULES([NCURSESW], [ncursesw], [ + CURSESINC="" + CURSESLIB="${NCURSESW_LIBS}" + CURSESLIBDIR= + CURSES_CFLAGS="${NCURSESW_CFLAGS}" + curseslib="ncursesw" + ], [ + AC_CHECK_PROG([ncursesw5_config], [ncursesw5-config], [yes]) + if test "$ncursesw5_config" = "yes"; then + CURSESINC="" + CURSESLIB=`ncursesw5-config --libs` + CURSESLIBDIR=`ncursesw5-config --libdir` + CURSES_CFLAGS=`ncursesw5-config --cflags` + curseslib="ncursesw" + else + AC_CHECK_LIB(ncursesw, initscr, + [ CURSESINC=''; CURSESLIB='-lncursesw'; curseslib="ncursesw"]) + fi + ]) + if test -n "$CURSESINC"; then + NCURSESLIBSUFFIX="w" + CURSES_NLS="yes" + fi + fi + if test "$curseslib" = "ncurses" -o "$curseslib" = "auto"; then + dnl First try out pkg-config, then fall back to old config scripts. + PKG_CHECK_MODULES([NCURSES], [ncurses], [ + CURSESINC="" + CURSESLIB="${NCURSES_LIBS}" + CURSESLIBDIR= + CURSES_CFLAGS="${NCURSES_CFLAGS}" + curseslib="ncurses" + ], [ + AC_CHECK_PROG([ncurses5_config], [ncurses5-config], [yes]) + if test "$ncurses5_config" = "yes"; then + CURSESINC="" + CURSESLIB=`ncurses5-config --libs` + CURSESLIBDIR=`ncurses5-config --libdir` + CURSES_CFLAGS=`ncurses5-config --cflags` + curseslib="ncurses" + else + AC_CHECK_LIB(ncurses, initscr, + [ CURSESINC=''; CURSESLIB='-lncurses'; curseslib="ncurses"]) + fi + ]) + fi + if test "$curseslib" = "curses" -o "$curseslib" = "auto"; then + AC_CHECK_LIB(curses, initscr, + [ CURSESINC=''; CURSESLIB='-lcurses'; curseslib="curses"]) + fi + if test -z "$CURSESINC"; then + AC_MSG_ERROR(this packages requires a curses library) + fi + + AC_MSG_CHECKING([for curses library]) + AC_MSG_RESULT([$curseslib]) + AC_MSG_CHECKING([for curses header name]) + AC_MSG_RESULT([$CURSESINC]) + AC_MSG_CHECKING([for curses compiler flags]) + AC_MSG_RESULT([$CURSES_CFLAGS]) + + dnl CURSESLIBS might have the library path at the beginning. If so, we cut it + dnl off so that we can insert the other curses libraries before the ncurses + dnl library but after the library path (which is later again prepended below). + if test -n "$CURSESLIBDIR"; then + if test "-L$CURSESLIBDIR " = "$(echo $CURSESLIB | cut -c-$((${#CURSESLIBDIR}+3)) )"; then + CURSESLIB="$(echo $CURSESLIB | cut -c$((${#CURSESLIBDIR}+4))-)" + fi + fi + + saved_CFLAGS="$CFLAGS" + saved_LDFLAGS="$LDFLAGS" + saved_LIBS="$LIBS" + CFLAGS="$CFLAGS $CURSES_CFLAGS" + if test -n "$CURSESLIBDIR"; then + LDFLAGS="$LDFLAGS -L$CURSESLIBDIR" + fi + LIBS="$CURSESLIB $LIBS" + + AC_TRY_LINK([#include ], [set_escdelay(100);],[HAVE_CURSES_ESCDELAY="yes"]) + if test "$HAVE_CURSES_ESCDELAY" = "yes"; then + AC_DEFINE([HAVE_CURSES_ESCDELAY], 1, [Have curses set_escdelay]) + fi + + if test "$USE_NLS" = "yes"; then + AC_MSG_CHECKING([for curses NLS support]) + dnl In theory, a single-byte curses works just fine in ISO 8859-* locales. + dnl In practice, however, everybody uses UTF-8 nowadays, so we'd better + dnl check for wide-character support. + dnl For ncurses/ncursesw, CURSES_NLS was already set above. + if test "$curseslib" = "curses"; then + AC_TRY_LINK([ + #define _XOPEN_SOURCE 1 + #define _XOPEN_SOURCE_EXTENDED 1 + #include + ], [ + cchar_t wc; + setcchar(&wc, L"x", A_NORMAL, 0, 0); + ], + [CURSES_NLS="yes"]) + fi + AC_MSG_RESULT([$CURSES_NLS]) + if test "$CURSES_NLS" = "yes"; then + AC_DEFINE([ENABLE_NLS_IN_CURSES], [1], + [Define if curses-based programs can show translated messages.]) + fi + fi + + AC_CHECK_HEADERS([panel.h menu.h form.h], [], + [AC_MSG_ERROR([required curses helper header not found])]) + AC_CHECK_LIB([panel$NCURSESLIBSUFFIX], [new_panel], + [CURSESLIB="-lpanel$NCURSESLIBSUFFIX $CURSESLIB"], + [AC_MSG_ERROR([panel$NCURSESLIBSUFFIX library not found])]) + AC_CHECK_LIB([menu$NCURSESLIBSUFFIX], [new_menu], + [CURSESLIB="-lmenu$NCURSESLIBSUFFIX $CURSESLIB"], + [AC_MSG_ERROR([menu$NCURSESLIBSUFFIX library not found])]) + AC_CHECK_LIB([form$NCURSESLIBSUFFIX], [new_form], + [CURSESLIB="-lform$NCURSESLIBSUFFIX $CURSESLIB"], + [AC_MSG_ERROR([form$NCURSESLIBSUFFIX library not found])]) + + CFLAGS="$saved_CFLAGS" + LDFLAGS="$saved_LDFLAGS" + LIBS="$saved_LIBS" + + if test -n "$CURSESLIBDIR"; then + CURSESLIB="-L$CURSESLIBDIR $CURSESLIB" + fi + + AC_MSG_CHECKING([for curses linker flags]) + AC_MSG_RESULT([$CURSESLIB]) +fi + +AC_SUBST(CURSESINC) +AC_SUBST(CURSESLIB) +AC_SUBST(CURSES_CFLAGS) + +test "x$prefix" = xNONE && prefix=$ac_default_prefix +test "x$exec_prefix" = xNONE && exec_prefix=$prefix + +eval dir="$datadir" +case "$dir" in +/*) ;; +*) dir="$prefix/share" +esac + +soundsdir="$dir/sounds/alsa" +AC_DEFINE_UNQUOTED(SOUNDSDIR, "$soundsdir", [directory containing sample data]) + +mydatadir="$dir/alsa" +AC_DEFINE_UNQUOTED(DATADIR, "$mydatadir", [directory containing alsa configuration]) +AC_SUBST(mydatadir) + +AC_ARG_WITH(testsound, + AS_HELP_STRING([--with-testsound=file], [give the path of test sound file for alsaconf]), + TESTSOUND="$withval", + TESTSOUND="$dir/test.wav") +AC_SUBST(TESTSOUND) + +AC_CONFIG_HEADERS(include/aconfig.h) + +dnl Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_C_INLINE +AC_HEADER_TIME + +dnl Checks for library functions. +AC_PROG_GCC_TRADITIONAL + +dnl Enable largefile support +AC_SYS_LARGEFILE + +SAVE_UTIL_VERSION + +AC_SUBST(LIBRT) + +dnl Check for systemd +PKG_CHECK_MODULES(SYSTEMD, [systemd >= 18], + [have_min_systemd="yes"], + [have_min_systemd="no"]) +AC_ARG_WITH([systemdsystemunitdir], + AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files]), + [], [with_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)]) +if test "x$with_systemdsystemunitdir" != xno; then + AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir]) +fi +AM_CONDITIONAL(HAVE_SYSTEMD, [test "$have_min_systemd" = "yes" \ + -a -n "$with_systemdsystemunitdir" -a "x$with_systemdsystemunitdir" != xno ]) + +AC_ARG_WITH([asound-state-dir], + AS_HELP_STRING([--with-asound-state-dir=DIR], [Directory to place asound.state file in]), + [ASOUND_STATE_DIR="$withval"], + [ASOUND_STATE_DIR="/var/lib/alsa"]) +AC_SUBST(ASOUND_STATE_DIR) + +AC_ARG_WITH([alsactl-lock-dir], + AS_HELP_STRING([--with-alsactl-lock-dir=DIR], [Directory to place lock files in]), + [ASOUND_LOCK_DIR="$withval"], + [ASOUND_LOCK_DIR="/var/lock"]) +AC_SUBST(ASOUND_LOCK_DIR) + +AC_ARG_WITH([alsactl-pidfile-dir], + AS_HELP_STRING([--with-alsactl-pidfile-dir=DIR], [Directory to place alsactl.pid file in]), + [ALSACTL_PIDFILE_DIR="$withval"], + [ALSACTL_PIDFILE_DIR="/var/run"]) +AC_SUBST(ALSACTL_PIDFILE_DIR) + +AC_ARG_WITH([alsactl-daemonswitch], + AS_HELP_STRING([--with-alsactl-daemonswitch=FILE], [File to test for the daemon mode]), + [ALSACTL_DAEMONSWITCH="$withval"], + [ALSACTL_DAEMONSWITCH="/etc/alsa/state-daemon.conf"]) +AC_SUBST(ALSACTL_DAEMONSWITCH) + +AC_ARG_WITH([alsactl-udev-extra-test], + AS_HELP_STRING([--with-alsactl-udev-extra-test=TEST], [Extra udev tests]), + [ALSACTL_UDEV_EXTRATEST="$withval"], + [ALSACTL_UDEV_EXTRATEST="default"]) +if test "$ALSACTL_UDEV_EXTRATEST" = "default"; then + ALSACTL_UDEV_EXTRATEST="TEST==\"${sbindir}\", TEST==\"${mydatadir}\"," +fi +AC_SUBST(ALSACTL_UDEV_EXTRATEST) + +AC_ARG_WITH([alsactl-udev-args], + AS_HELP_STRING([--with-alsactl-udev-args=ARGS], [Extra alsactl arguments (udev rules)]), + [ALSACTL_UDEV_ARGS="$withval"], + [ALSACTL_UDEV_ARGS=""]) +AC_SUBST(ALSACTL_UDEV_ARGS) + +dnl pre-process plugin directory +AC_ARG_WITH(plugindir, + AS_HELP_STRING([--with-plugindir=dir], + [path where pre-process plugin files are stored]), + plugindir="$withval", plugindir="") +if test -z "$plugindir"; then + eval dir="$libdir" + echo ${exec_prefix} + echo $libdir + echo $dir + case "$dir" in + /*) ;; + *) dir="$dir" + esac + plugindir="$dir/alsa-topology" +fi +AC_DEFINE_UNQUOTED(ALSA_TOPOLOGY_PLUGIN_DIR, "$plugindir", [directory containing ALSA topology pre-process plugins]) +ALSA_TOPOLOGY_PLUGIN_DIR="$plugindir" +AC_SUBST(ALSA_TOPOLOGY_PLUGIN_DIR) + +AC_OUTPUT(Makefile alsactl/Makefile alsactl/init/Makefile \ + alsamixer/Makefile amidi/Makefile amixer/Makefile \ + m4/Makefile po/Makefile.in \ + alsaconf/alsaconf alsaconf/Makefile \ + alsaconf/po/Makefile \ + alsaucm/Makefile topology/Makefile topology/nhlt/Makefile \ + bat/Makefile bat/tests/Makefile bat/tests/asound_state/Makefile \ + aplay/Makefile include/Makefile iecset/Makefile utils/Makefile \ + utils/alsa-utils.spec seq/Makefile seq/aconnect/Makefile \ + seq/aplaymidi/Makefile seq/aplaymidi2/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \ + seq/aseqsend/Makefile speaker-test/Makefile speaker-test/samples/Makefile \ + alsaloop/Makefile alsa-info/Makefile \ + axfer/Makefile axfer/test/Makefile \ + nhlt/Makefile) diff --git a/gitcompile b/gitcompile new file mode 100755 index 0000000..19dd1d5 --- /dev/null +++ b/gitcompile @@ -0,0 +1,28 @@ +#!/bin/bash + +if test -d ../alsa-lib/utils && ! test -r `aclocal --print-ac-dir`/alsa.m4; then + ACLOCAL_FLAGS="$ACLOCAL_FLAGS -I ../alsa-lib/utils" +fi +aclocal $ACLOCAL_FLAGS +# save original files to avoid stupid modifications by gettextize +cp Makefile.am Makefile.am.ok +cp configure.ac configure.ac.ok +gettextize -c -f --no-changelog +echo "EXTRA_DIST = gettext.m4" > m4/Makefile.am +cp Makefile.am.ok Makefile.am +cp configure.ac.ok configure.ac +touch ltconfig +libtoolize --force --copy --automake +aclocal $ACLOCAL_FLAGS +autoheader +automake --foreign --copy --add-missing +touch depcomp # for older automake +autoconf +export CFLAGS='-O2 -Wall -W -Wunused-const-variable=0 -pipe -g' +echo "CFLAGS=$CFLAGS" +echo "./configure $@" +./configure $@ || exit 1 +unset CFLAGS +if [ -z "$GITCOMPILE_NO_MAKE" ]; then + make +fi diff --git a/iecset/Makefile.am b/iecset/Makefile.am new file mode 100644 index 0000000..8c80f90 --- /dev/null +++ b/iecset/Makefile.am @@ -0,0 +1,9 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +LDADD = -lm +# LDFLAGS = -static +# CFLAGS += -g -Wall + +bin_PROGRAMS = iecset +iecset_SOURCES = iecset.c iecbits.c +man_MANS = iecset.1 +EXTRA_DIST = iecset.1 diff --git a/iecset/iecbits.c b/iecset/iecbits.c new file mode 100644 index 0000000..4a768a6 --- /dev/null +++ b/iecset/iecbits.c @@ -0,0 +1,259 @@ +/* + iecdump - dump IEC958 status bits on ALSA + Copyright (C) 2003 by Takashi Iwai + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include + +struct category_str { + int val; + const char *name; +}; + +static const struct category_str con_category[] = { + { IEC958_AES1_CON_GENERAL, "general" }, + + { IEC958_AES1_CON_IEC908_CD, "CD" }, + { IEC958_AES1_CON_NON_IEC908_CD, "non-IEC908 CD" }, + { IEC958_AES1_CON_MINI_DISC, "Mini-Disc" }, + { IEC958_AES1_CON_DVD, "DVD" }, + + { IEC958_AES1_CON_PCM_CODER, "PCM coder" }, + { IEC958_AES1_CON_MIXER, "digital signal mixer" }, + { IEC958_AES1_CON_RATE_CONVERTER, "rate converter" }, + { IEC958_AES1_CON_SAMPLER, "sampler" }, + { IEC958_AES1_CON_DSP, "digital sound processor" }, + + { IEC958_AES1_CON_DAT, "DAT" }, + { IEC958_AES1_CON_VCR, "VCR" }, + { IEC958_AES1_CON_DCC, "DCC" }, + { IEC958_AES1_CON_MAGNETIC_DISC, "magnetic disc" }, + + { IEC958_AES1_CON_DAB_JAPAN, "digital audio broadcast (Japan)" }, + { IEC958_AES1_CON_DAB_EUROPE, "digital audio broadcast (Europe)" }, + { IEC958_AES1_CON_DAB_USA, "digital audio broadcast (USA)" }, + { IEC958_AES1_CON_SOFTWARE, "software delivery" }, + + { IEC958_AES1_CON_SYNTHESIZER, "synthesizer" }, + { IEC958_AES1_CON_MICROPHONE, "microphone" }, + + { IEC958_AES1_CON_ADC, "ADC without copyright information" }, + + { IEC958_AES1_CON_ADC_COPYRIGHT, "ADC with copyright information" }, + + { IEC958_AES1_CON_SOLIDMEM_DIGITAL_RECORDER_PLAYER, "flash memory recorder/player" }, + + { IEC958_AES1_CON_EXPERIMENTAL, "experimental" }, +}; + + +#define ARRAY_SIZE(x) (int)(sizeof(x)/sizeof(x[0])) + +void dump_iec958(snd_aes_iec958_t *iec) +{ + int i; + + if (! (iec->status[0] & IEC958_AES0_PROFESSIONAL)) { + /* consumer */ + printf("Mode: consumer\n"); + printf("Data: "); + if (!(iec->status[0] & IEC958_AES0_NONAUDIO)) { + printf("audio\n"); + } else { + printf("non-audio\n"); + } + printf("Rate: "); + switch (iec->status[3] & IEC958_AES3_CON_FS) { + case IEC958_AES3_CON_FS_22050: + printf("22050 Hz\n"); + break; + case IEC958_AES3_CON_FS_24000: + printf("24000 Hz\n"); + break; + case IEC958_AES3_CON_FS_32000: + printf("32000 Hz\n"); + break; + case IEC958_AES3_CON_FS_44100: + printf("44100 Hz\n"); + break; + case IEC958_AES3_CON_FS_48000: + printf("48000 Hz\n"); + break; + case IEC958_AES3_CON_FS_88200: + printf("88200 Hz\n"); + break; + case IEC958_AES3_CON_FS_96000: + printf("96000 Hz\n"); + break; + case IEC958_AES3_CON_FS_176400: + printf("176400 Hz\n"); + break; + case IEC958_AES3_CON_FS_192000: + printf("192000 Hz\n"); + break; + case IEC958_AES3_CON_FS_768000: + printf("768000 Hz\n"); + break; + case IEC958_AES3_CON_FS_NOTID: + printf("not indicated\n"); + break; + default: + printf("unknown\n"); + break; + } + printf("Copyright: "); + if (iec->status[0] & IEC958_AES0_CON_NOT_COPYRIGHT) { + printf("permitted\n"); + } else { + printf("protected\n"); + } + printf("Emphasis: "); + if ((iec->status[0] & IEC958_AES0_CON_EMPHASIS) != IEC958_AES0_CON_EMPHASIS_5015) { + printf("none\n"); + } else { + printf("50/15us\n"); + } + printf("Category: "); + for (i = 0; i < ARRAY_SIZE(con_category); i++) { + if ((iec->status[1] & IEC958_AES1_CON_CATEGORY) == con_category[i].val) { + printf("%s\n", con_category[i].name); + break; + } + } + if (i >= ARRAY_SIZE(con_category)) { + printf("unknown 0x%x\n", iec->status[1] & IEC958_AES1_CON_CATEGORY); + } + printf("Original: "); + if (iec->status[1] & IEC958_AES1_CON_ORIGINAL) { + printf("original\n"); + } else { + printf("1st generation\n"); + } + printf("Clock: "); + switch (iec->status[3] & IEC958_AES3_CON_CLOCK) { + case IEC958_AES3_CON_CLOCK_1000PPM: + printf("1000 ppm\n"); + break; + case IEC958_AES3_CON_CLOCK_50PPM: + printf("50 ppm\n"); + break; + case IEC958_AES3_CON_CLOCK_VARIABLE: + printf("variable pitch\n"); + break; + default: + printf("unknown\n"); + break; + } + } else { + printf("Mode: professional\n"); + printf("Data: "); + if (!(iec->status[0] & IEC958_AES0_NONAUDIO)) { + printf("audio\n"); + } else { + printf("non-audio\n"); + } + printf("Rate: "); + switch (iec->status[0] & IEC958_AES0_PRO_FS) { + case IEC958_AES0_PRO_FS_44100: + printf("44100 Hz\n"); + break; + case IEC958_AES0_PRO_FS_48000: + printf("48000 Hz\n"); + break; + case IEC958_AES0_PRO_FS_32000: + printf("32000 Hz\n"); + break; + default: + printf("unknown\n"); + break; + } + printf("Rate Locked: "); + if (iec->status[0] & IEC958_AES0_PRO_FREQ_UNLOCKED) + printf("no\n"); + else + printf("yes\n"); + printf("Emphasis: "); + switch (iec->status[0] & IEC958_AES0_PRO_EMPHASIS) { + case IEC958_AES0_PRO_EMPHASIS_CCITT: + printf("CCITT J.17\n"); + break; + case IEC958_AES0_PRO_EMPHASIS_NONE: + printf("none\n"); + break; + case IEC958_AES0_PRO_EMPHASIS_5015: + printf("50/15us\n"); + break; + case IEC958_AES0_PRO_EMPHASIS_NOTID: + default: + printf("unknown\n"); + break; + } + printf("Stereophonic: "); + if ((iec->status[1] & IEC958_AES1_PRO_MODE) == IEC958_AES1_PRO_MODE_STEREOPHONIC) { + printf("stereo\n"); + } else { + printf("not indicated\n"); + } + printf("Userbits: "); + switch (iec->status[1] & IEC958_AES1_PRO_USERBITS) { + case IEC958_AES1_PRO_USERBITS_192: + printf("192bit\n"); + break; + case IEC958_AES1_PRO_USERBITS_UDEF: + printf("user-defined\n"); + break; + default: + printf("unknown\n"); + break; + } + printf("Sample Bits: "); + switch (iec->status[2] & IEC958_AES2_PRO_SBITS) { + case IEC958_AES2_PRO_SBITS_20: + printf("20 bit\n"); + break; + case IEC958_AES2_PRO_SBITS_24: + printf("24 bit\n"); + break; + case IEC958_AES2_PRO_SBITS_UDEF: + printf("user defined\n"); + break; + default: + printf("unknown\n"); + break; + } + printf("Word Length: "); + switch (iec->status[2] & IEC958_AES2_PRO_WORDLEN) { + case IEC958_AES2_PRO_WORDLEN_22_18: + printf("22 bit or 18 bit\n"); + break; + case IEC958_AES2_PRO_WORDLEN_23_19: + printf("23 bit or 19 bit\n"); + break; + case IEC958_AES2_PRO_WORDLEN_24_20: + printf("24 bit or 20 bit\n"); + break; + case IEC958_AES2_PRO_WORDLEN_20_16: + printf("20 bit or 16 bit\n"); + break; + default: + printf("unknown\n"); + break; + } + } +} + diff --git a/iecset/iecset.1 b/iecset/iecset.1 new file mode 100644 index 0000000..dec4ac0 --- /dev/null +++ b/iecset/iecset.1 @@ -0,0 +1,104 @@ +.TH iecset 1 "23 Oct 2003" +.SH NAME +iecset \- Set or dump IEC958 status bits + +.SH SYNOPSIS +\fBiecset\fP [\fIoptions\fP] [\fIcmd\fP \fIarg\fP...] + +.SH DESCRIPTION +\fBiecset\fP is a small utility to set or dump the IEC958 (or so-called +"S/PDIF") status bits of the specified sound card via ALSA control API. + +When \fBiecset\fP is started without arguments except for options, +it will show the current IEC958 status in a human-readable form. +When the commands are given in the arguments, they are parsed +and the IEC958 status bits are updated. The resultant status is +shown as well. + +The commands consist of the command directive and the argument. +As the boolean argument, \fIyes\fP, \fIno\fP, \fItrue\fP, \fIfalse\fP, +or a digit number is allowed. + +.SH EXAMPLES +.TP +.BI iecset\ \-Dhw:1 +Displays the current IEC958 status bits on the second card. +This is equivalent with \fI\-c 1\fP. +.TP +.BI iecset\ \-x +Displays the current IEC958 status bits in a style of the arguments +for the PCM stream. The output string can be passed to the \fIiec958\fP +(or \fIspdif\fP) PCM type as the optional argument. +.TP +.BI iecset\ pro\ off\ audio\ off +Sets the current status to the consumer-mode and turns on the +non-audio bit. The modified status will be shown, too. + +.SH OPTIONS +.TP +\fI\-D\fP device +Specifies the device name of the control to open +.TP +\fI\-c\fP card +Specifies the card index to open. Equivalent with \fI\-Dhw:x\fP. +.TP +\fI\-n\fP index +Specifies the IEC958 control element index, in case you have multiple +IEC958 devices and need to choose one of them. +.TP +\fI\-x\fP +Dumps the status in the form of AESx bytes. +.TP +\fI\-i\fP +Reads the command sequences from stdin. +Each line has single command. + +.SH COMMANDS +.TP +\fIprofessional\fP +The professional mode (true) or consumer mode (false). + +.TP +\fIaudio\fP +The audio mode (true) or non-audio mode (false). + +.TP +\fIrate\fP +The sample rate in Hz. + +.TP +\fIemphasis\fP +The emphasis: 0 = none, 1 = 50/15us, 2 = CCITT. + +.TP +\fIlock\fP +Rate lock: locked (true), unlocked (false). +This command is for the professional mode only. + +.TP +\fIsbits\fP +Sample bits: 2 = 20bit, 4 = 24bit, 6 = undefined. +This command is for the professional mode only. + +.TP +\fIwordlength\fP +Wordlength: 0 = No, 2 = 22-18 bit, 4 = 23-19 bit, 5 = 24-20 bit, 6 = 20-16 bit. +This command is for the professional mode only. + +.TP +\fIcategory\fP +Category: the value is from 0 to 0x7f. +This command is for the consumer mode only. + +.TP +\fIcopyright\fP +Copyright: copyrighted (true), non-copyrighted (false). +This command is for the consumer mode only. + +.TP +\fIoriginal\fP +Original flag: original (true), 1st generation (false). +This command is for the consumer mode only. + +.SH AUTHOR +Takashi Iwai diff --git a/iecset/iecset.c b/iecset/iecset.c new file mode 100644 index 0000000..65e9425 --- /dev/null +++ b/iecset/iecset.c @@ -0,0 +1,433 @@ +/* + iecset - change IEC958 status bits on ALSA + Copyright (C) 2003 by Takashi Iwai + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "aconfig.h" +#include +#include +#include + +void dump_iec958(snd_aes_iec958_t *iec); + +static int get_bool(const char *str) +{ + if (strncmp(str, "yes", 3) == 0 || + strncmp(str, "YES", 3) == 0 || + strncmp(str, "on", 2) == 0 || + strncmp(str, "ON", 2) == 0 || + strncmp(str, "true", 4) == 0 || + strncmp(str, "TRUE", 4) == 0 || + *str == '1') + return 1; + return 0; +} + +enum { + CMD_BOOL, CMD_BOOL_INV, CMD_INT +}; + +enum { + IDX_PRO, IDX_NOAUDIO, IDX_RATE, IDX_UNLOCK, IDX_SBITS, IDX_WORD, IDX_EMP, IDX_CAT, IDX_NOCOPY, IDX_ORIG, + IDX_LAST +}; + +struct cmdtbl { + const char *name; + int idx; + int type; + const char *desc; +}; + +static const struct cmdtbl cmds[] = { + { "pro", IDX_PRO, CMD_BOOL, + "professional (common)\n\toff = consumer mode, on = professional mode" }, + { "aud", IDX_NOAUDIO, CMD_BOOL_INV, + "audio (common)\n\ton = audio mode, off = non-audio mode" }, + { "rat", IDX_RATE, CMD_INT, + "rate (common)\n\tsample rate in Hz (0 = not indicated)" }, + { "emp", IDX_EMP, CMD_INT, + "emphasis (common)\n\t0 = none, 1 = 50/15us, 2 = CCITT" }, + { "loc", IDX_UNLOCK, CMD_BOOL_INV, + "lock (prof.)\n\toff = rate unlocked, on = rate locked" }, + { "sbi", IDX_SBITS, CMD_INT, + "sbits (prof.)\n\tsample bits 2 = 20bit, 4 = 24bit, 6 = undef" }, + { "wor", IDX_WORD, CMD_INT, + "wordlength (prof.)\n\t0=no, 2=22-18bit, 4=23-19bit, 5=24-20bit, 6=20-16bit" }, + { "cat", IDX_CAT, CMD_INT, + "category (consumer)\n\t0-0x7f" }, + { "cop", IDX_NOCOPY, CMD_BOOL_INV, + "copyright (consumer)\n\toff = non-copyright, on = copyright" }, + { "ori", IDX_ORIG, CMD_BOOL, + "original (consumer)\n\toff = 1st-gen, on = original" }, +}; + + +static void error(const char *s, int err) +{ + fprintf(stderr, "%s: %s\n", s, snd_strerror(err)); +} + + +static void usage(void) +{ + int i; + + printf("Usage: iecset [options] [cmd arg...]\n"); + printf("Options:\n"); + printf(" -D device specifies the control device to use\n"); + printf(" -c card specifies the card number to use (equiv. with -Dhw:#)\n"); + printf(" -n number specifies the control index number (default = 0)\n"); + printf(" -x dump the dump the AESx hex code for IEC958 PCM parameters\n"); + printf(" -i read commands from stdin\n"); + printf("Commands:\n"); + for (i = 0; i < (int)(sizeof(cmds)/sizeof(cmds[0])); i++) { + printf(" %s\n", cmds[i].desc); + } +} + + +/* + * parse iecset commands + */ +static void parse_command(int *parms, const char *c, const char *arg) +{ + int i; + + for (i = 0; i < (int)(sizeof(cmds)/sizeof(cmds[0])); i++) { + if (strncmp(c, cmds[i].name, strlen(cmds[i].name)) == 0) { + int val; + switch (cmds[i].type) { + case CMD_BOOL: + val = get_bool(arg); + break; + case CMD_BOOL_INV: + val = !get_bool(arg); + break; + case CMD_INT: + default: + val = (int)strtol(arg, NULL, 0); + break; + } + parms[cmds[i].idx] = val; + return; + } + } +} + +static char *skipspace(char *line) +{ + char *p; + for (p = line; *p && isspace(*p); p++) + ; + return p; +} + +/* + * parse iecset commands from the file + */ +static void parse_file(int *parms, FILE *fp) +{ + char line[1024], *cmd, *arg; + while (fgets(line, sizeof(line), fp) != NULL) { + cmd = skipspace(line); + if (*cmd == '#' || ! *cmd) + continue; + for (arg = cmd; *arg && !isspace(*arg); arg++) + ; + if (! *arg) + continue; + *arg++ = 0; + arg = skipspace(arg); + if (! *arg) + continue; + parse_command(parms, cmd, arg); + } +} + +/* update iec958 status values + * return non-zero if the values are modified + */ +static int update_iec958_status(snd_aes_iec958_t *iec958, int *parms) +{ + int changed = 0; + if (parms[IDX_PRO] >= 0) { + if (parms[IDX_PRO]) + iec958->status[0] |= IEC958_AES0_PROFESSIONAL; + else + iec958->status[0] &= ~IEC958_AES0_PROFESSIONAL; + changed = 1; + } + if (parms[IDX_NOAUDIO] >= 0) { + if (parms[IDX_NOAUDIO]) + iec958->status[0] |= IEC958_AES0_NONAUDIO; + else + iec958->status[0] &= ~IEC958_AES0_NONAUDIO; + changed = 1; + } + if (parms[IDX_RATE] >= 0) { + if (iec958->status[0] & IEC958_AES0_PROFESSIONAL) { + iec958->status[0] &= ~IEC958_AES0_PRO_FS; + switch (parms[IDX_RATE]) { + case 44100: + iec958->status[0] |= IEC958_AES0_PRO_FS_44100; + break; + case 48000: + iec958->status[0] |= IEC958_AES0_PRO_FS_48000; + break; + case 32000: + iec958->status[0] |= IEC958_AES0_PRO_FS_32000; + break; + } + } else { + iec958->status[3] &= ~IEC958_AES3_CON_FS; + switch (parms[IDX_RATE]) { + case 22050: + iec958->status[3] |= IEC958_AES3_CON_FS_22050; + break; + case 24000: + iec958->status[3] |= IEC958_AES3_CON_FS_24000; + break; + case 32000: + iec958->status[3] |= IEC958_AES3_CON_FS_32000; + break; + case 44100: + iec958->status[3] |= IEC958_AES3_CON_FS_44100; + break; + case 48000: + iec958->status[3] |= IEC958_AES3_CON_FS_48000; + break; + case 88200: + iec958->status[3] |= IEC958_AES3_CON_FS_88200;; + break; + case 96000: + iec958->status[3] |= IEC958_AES3_CON_FS_96000; + break; + case 176400: + iec958->status[3] |= IEC958_AES3_CON_FS_176400; + break; + case 192000: + iec958->status[3] |= IEC958_AES3_CON_FS_192000; + break; + case 768000: + iec958->status[3] |= IEC958_AES3_CON_FS_768000; + break; + default: + iec958->status[3] |= IEC958_AES3_CON_FS_NOTID; + break; + } + } + changed = 1; + } + if (parms[IDX_NOCOPY] >= 0) { + if (! (iec958->status[0] & IEC958_AES0_PROFESSIONAL)) { + if (parms[IDX_NOCOPY]) + iec958->status[0] |= IEC958_AES0_CON_NOT_COPYRIGHT; + else + iec958->status[0] &= ~IEC958_AES0_CON_NOT_COPYRIGHT; + } + changed = 1; + } + if (parms[IDX_ORIG] >= 0) { + if (! (iec958->status[0] & IEC958_AES0_PROFESSIONAL)) { + if (parms[IDX_ORIG]) + iec958->status[1] |= IEC958_AES1_CON_ORIGINAL; + else + iec958->status[1] &= ~IEC958_AES1_CON_ORIGINAL; + } + changed = 1; + } + if (parms[IDX_EMP] >= 0) { + if (iec958->status[0] & IEC958_AES0_PROFESSIONAL) { + iec958->status[0] &= ~IEC958_AES0_PRO_EMPHASIS; + switch (parms[IDX_EMP]) { + case 0: + iec958->status[0] |= IEC958_AES0_PRO_EMPHASIS_NONE; + break; + case 1: + iec958->status[0] |= IEC958_AES0_PRO_EMPHASIS_5015; + break; + case 2: + iec958->status[0] |= IEC958_AES0_PRO_EMPHASIS_CCITT; + break; + } + } else { + if (parms[IDX_EMP]) + iec958->status[0] |= IEC958_AES0_CON_EMPHASIS_5015; + else + iec958->status[0] &= ~IEC958_AES0_CON_EMPHASIS_5015; + } + changed = 1; + } + if (parms[IDX_UNLOCK] >= 0) { + if (iec958->status[0] & IEC958_AES0_PROFESSIONAL) { + if (parms[IDX_UNLOCK]) + iec958->status[0] |= IEC958_AES0_PRO_FREQ_UNLOCKED; + else + iec958->status[0] &= ~IEC958_AES0_PRO_FREQ_UNLOCKED; + } + changed = 1; + } + if (parms[IDX_SBITS] >= 0) { + if (iec958->status[0] & IEC958_AES0_PROFESSIONAL) { + iec958->status[2] &= ~IEC958_AES2_PRO_SBITS; + iec958->status[2] |= parms[IDX_SBITS] & 7; + } + changed = 1; + } + if (parms[IDX_WORD] >= 0) { + if (iec958->status[0] & IEC958_AES0_PROFESSIONAL) { + iec958->status[2] &= ~IEC958_AES2_PRO_WORDLEN; + iec958->status[2] |= (parms[IDX_WORD] & 7) << 3; + } + changed = 1; + } + if (parms[IDX_CAT] >= 0) { + if (! (iec958->status[0] & IEC958_AES0_PROFESSIONAL)) { + iec958->status[1] &= ~IEC958_AES1_CON_CATEGORY; + iec958->status[1] |= parms[IDX_CAT] & 0x7f; + } + changed = 1; + } + + return changed; +} + + +int main(int argc, char **argv) +{ + const char *dev = "default"; + const char *spdif_str = SND_CTL_NAME_IEC958("", PLAYBACK, DEFAULT); + int spdif_index = -1; + snd_ctl_t *ctl; + snd_ctl_elem_list_t *clist; + snd_ctl_elem_id_t *cid; + snd_ctl_elem_value_t *cval; + snd_aes_iec958_t iec958; + int from_stdin = 0; + int dumphex = 0; + int i, c, err; + unsigned int controls, cidx; + char tmpname[32]; + int parms[IDX_LAST]; + + for (i = 0; i < IDX_LAST; i++) + parms[i] = -1; /* not set */ + + while ((c = getopt(argc, argv, "D:c:n:xhi")) != -1) { + switch (c) { + case 'D': + dev = optarg; + break; + case 'c': + i = atoi(optarg); + if (i < 0 || i >= 32) { + fprintf(stderr, "invalid card index %d\n", i); + return 1; + } + sprintf(tmpname, "hw:%d", i); + dev = tmpname; + break; + case 'n': + spdif_index = atoi(optarg); + break; + case 'x': + dumphex = 1; + break; + case 'i': + from_stdin = 1; + break; + default: + usage(); + return 1; + } + } + + if ((err = snd_ctl_open(&ctl, dev, 0)) < 0) { + error("snd_ctl_open", err); + return 1; + } + + snd_ctl_elem_list_alloca(&clist); + if ((err = snd_ctl_elem_list(ctl, clist)) < 0) { + error("snd_ctl_elem_list", err); + return 1; + } + if ((err = snd_ctl_elem_list_alloc_space(clist, snd_ctl_elem_list_get_count(clist))) < 0) { + error("snd_ctl_elem_list_alloc_space", err); + return 1; + } + if ((err = snd_ctl_elem_list(ctl, clist)) < 0) { + error("snd_ctl_elem_list", err); + return 1; + } + + controls = snd_ctl_elem_list_get_used(clist); + for (cidx = 0; cidx < controls; cidx++) { + if (!strcmp(snd_ctl_elem_list_get_name(clist, cidx), spdif_str)) + if (spdif_index < 0 || + (int)snd_ctl_elem_list_get_index(clist, cidx) == spdif_index) + break; + } + if (cidx >= controls) { + fprintf(stderr, "control \"%s\" (index %d) not found\n", + spdif_str, spdif_index); + return 1; + } + + snd_ctl_elem_id_alloca(&cid); + snd_ctl_elem_list_get_id(clist, cidx, cid); + snd_ctl_elem_value_alloca(&cval); + snd_ctl_elem_value_set_id(cval, cid); + if ((err = snd_ctl_elem_read(ctl, cval)) < 0) { + error("snd_ctl_elem_read", err); + return 1; + } + + snd_ctl_elem_value_get_iec958(cval, &iec958); + + /* parse from stdin */ + if (from_stdin) + parse_file(parms, stdin); + + /* parse commands */ + for (c = optind; c < argc - 1; c += 2) + parse_command(parms, argv[c], argv[c + 1]); + + if (update_iec958_status(&iec958, parms)) { + /* store the values */ + snd_ctl_elem_value_set_iec958(cval, &iec958); + if ((err = snd_ctl_elem_write(ctl, cval)) < 0) { + error("snd_ctl_elem_write", err); + return 1; + } + if ((err = snd_ctl_elem_read(ctl, cval)) < 0) { + error("snd_ctl_elem_write", err); + return 1; + } + snd_ctl_elem_value_get_iec958(cval, &iec958); + } + + if (dumphex) + printf("AES0=0x%02x,AES1=0x%02x,AES2=0x%02x,AES3=0x%02x\n", + iec958.status[0], iec958.status[1], iec958.status[2], iec958.status[3]); + else + dump_iec958(&iec958); + + snd_ctl_close(ctl); + return 0; +} diff --git a/include/Makefile.am b/include/Makefile.am new file mode 100644 index 0000000..3c73568 --- /dev/null +++ b/include/Makefile.am @@ -0,0 +1,25 @@ +noinst_HEADERS=version.h gettext.h gettext_curses.h bswap.h os_compat.h + +version.h: stamp-vh + @: + +stamp-vh: $(top_builddir)/configure.ac + @echo "/*" > ver.tmp + @echo " * version.h" >> ver.tmp + @echo " */" >> ver.tmp + @echo "" >> ver.tmp + @echo "#define SND_UTIL_MAJOR $(SND_UTIL_MAJOR)" >> ver.tmp + @echo "#define SND_UTIL_MINOR $(SND_UTIL_MINOR)" >> ver.tmp + @echo "#define SND_UTIL_SUBMINOR $(SND_UTIL_SUBMINOR)" >> ver.tmp + @echo "#define SND_UTIL_VERSION ((SND_UTIL_MAJOR<<16)|\\" >> ver.tmp + @echo " (SND_UTIL_MINOR<<8)|\\" >> ver.tmp + @echo " SND_UTIL_SUBMINOR)" >> ver.tmp + @echo "#define SND_UTIL_VERSION_STR \"$(SND_UTIL_VERSION)\"" >> ver.tmp + @echo >> ver.tmp + @cmp -s version.h ver.tmp \ + || (echo "Updating version.h"; \ + cp ver.tmp version.h; \ + echo timestamp > stamp-vh) + -@rm -f ver.tmp + +AM_CPPFLAGS=-I$(top_srcdir)/include diff --git a/include/aconfig.h b/include/aconfig.h new file mode 100644 index 0000000..2a3157e --- /dev/null +++ b/include/aconfig.h @@ -0,0 +1,186 @@ +/* include/aconfig.h. Generated from aconfig.h.in by configure. */ +/* include/aconfig.h.in. Generated from configure.ac by autoheader. */ + +/* directory containing ALSA topology pre-process plugins */ +#define ALSA_TOPOLOGY_PLUGIN_DIR "/usr/lib/alsa-topology" + +/* directory containing alsa configuration */ +#define DATADIR "/vendor/usr/share/alsa" + +/* Define to 1 if translation of program messages to the user's native + language is requested. */ +/* #undef ENABLE_NLS */ + +/* Define if curses-based programs can show translated messages. */ +/* #undef ENABLE_NLS_IN_CURSES */ + +/* Define to 1 if you have the header file. */ +#define HAVE_ALSA_MIXER_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ALSA_PCM_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ALSA_RAWMIDI_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ALSA_SEQ_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ALSA_TOPOLOGY_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ALSA_USE_CASE_H 1 + +/* Define to 1 if you have the Mac OS X function + CFLocaleCopyPreferredLanguages in the CoreFoundation framework. */ +/* #undef HAVE_CFLOCALECOPYPREFERREDLANGUAGES */ + +/* Define to 1 if you have the Mac OS X function CFPreferencesCopyAppValue in + the CoreFoundation framework. */ +/* #undef HAVE_CFPREFERENCESCOPYAPPVALUE */ + +/* Have clock gettime */ +/* #undef HAVE_CLOCK_GETTIME */ + +/* Have curses set_escdelay */ +/* #undef HAVE_CURSES_ESCDELAY */ + +/* Define if the GNU dcgettext() function is already present or preinstalled. + */ +/* #undef HAVE_DCGETTEXT */ + +/* Define to 1 if you have the header file. */ +#define HAVE_DLFCN_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_FORM_H */ + +/* Define if the GNU gettext() function is already present or preinstalled. */ +/* #undef HAVE_GETTEXT */ + +/* Define if you have the iconv() function and it works. */ +#define HAVE_ICONV 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have the `asound' library (-lasound). */ +/* #undef HAVE_LIBASOUND */ + +/* Define to 1 if you have the `atopology' library (-latopology). */ +/* #undef HAVE_LIBATOPOLOGY */ + +/* Define to 1 if you have the `fftw3f' library (-lfftw3f). */ +/* #undef HAVE_LIBFFTW3F */ + +/* Define to 1 if you have the `m' library (-lm). */ +/* #undef HAVE_LIBM */ + +/* Define to 1 if you have the `pthread' library (-lpthread). */ +/* #undef HAVE_LIBPTHREAD */ + +/* Have librt */ +/* #undef HAVE_LIBRT */ + +/* Define to 1 if you have the `tinyalsa' library (-ltinyalsa). */ +/* #undef HAVE_LIBTINYALSA */ + +/* Define to 1 if you have the header file. */ +#define HAVE_MALLOC_H 1 + +/* Define if Linux kernel supports memfd_create system call */ +/* #undef HAVE_MEMFD_CREATE */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_MENU_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_PANEL_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SAMPLERATE_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDIO_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TIME_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UNISTD_H 1 + +/* Define to the sub-directory where libtool stores uninstalled libraries. */ +#define LT_OBJDIR ".libs/" + +/* Name of package */ +#define PACKAGE "alsa-utils" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "alsa-utils" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "alsa-utils 1.2.14" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "alsa-utils" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "1.2.14" + +/* directory containing sample data */ +#define SOUNDSDIR "/vendor/usr/share/sounds/alsa" + +/* Define to 1 if all of the C90 standard headers exist (not just the ones + required in a freestanding environment). This macro is provided for + backward compatibility; new code need not use it. */ +#define STDC_HEADERS 1 + +/* Define to 1 if you can safely include both and . This + macro is obsolete. */ +#define TIME_WITH_SYS_TIME 1 + +/* ALSA util version */ +#define VERSION "1.2.14" + +/* Define if FFADO library is available */ +/* #undef WITH_FFADO */ + +/* Number of bits in a file offset, on hosts where this is settable. */ +/* #undef _FILE_OFFSET_BITS */ + +/* Define for large files, on AIX-style hosts. */ +/* #undef _LARGE_FILES */ + +/* Define to empty if `const' does not conform to ANSI C. */ +/* #undef const */ + +/* Define to `__inline__' or `__inline' if that's what the C compiler + calls it, or to nothing if 'inline' is not supported under any name. */ +#ifndef __cplusplus +/* #undef inline */ +#endif diff --git a/include/bswap.h b/include/bswap.h new file mode 100644 index 0000000..e590124 --- /dev/null +++ b/include/bswap.h @@ -0,0 +1,44 @@ +/* + * ALSA lib - compatibility header for providing byte swapping macros + * Copyright (c) 2016 by Thomas Klausner + * + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __BSWAP_H +#define __BSWAP_H + +#if defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) +#include +#define bswap_16 bswap16 +#define bswap_32 bswap32 +#define bswap_64 bswap64 +#elif defined(__OpenBSD__) +#include +#define bswap_16 swap16 +#define bswap_32 swap32 +#define bswap_64 swap64 +#elif defined (__sun) +#include +#define bswap_16 BSWAP_16 +#define bswap_32 BSWAP_32 +#define bswap_64 BSWAP_64 +#else +#include +#endif + +#endif diff --git a/include/gettext.h b/include/gettext.h new file mode 100644 index 0000000..d8a1467 --- /dev/null +++ b/include/gettext.h @@ -0,0 +1,22 @@ +#ifndef __MY_GETTEXT_H +#define __MY_GETTEXT_H + +#ifdef USES_CURSES +#define ENABLE_NLS_TEST ENABLE_NLS_IN_CURSES +#else +#define ENABLE_NLS_TEST ENABLE_NLS +#endif + +#if ENABLE_NLS_TEST +# include +#else +# define gettext(msgid) (msgid) +# define textdomain(domain) +# define bindtextdomain(domain, dir) +#endif + +#define _(msgid) gettext (msgid) +#define gettext_noop(msgid) msgid +#define N_(msgid) gettext_noop (msgid) + +#endif /* __MY_GETTEXT_H */ diff --git a/include/gettext_curses.h b/include/gettext_curses.h new file mode 100644 index 0000000..f1c4041 --- /dev/null +++ b/include/gettext_curses.h @@ -0,0 +1,2 @@ +#define USES_CURSES +#include "gettext.h" diff --git a/include/os_compat.h b/include/os_compat.h new file mode 100644 index 0000000..8d4f041 --- /dev/null +++ b/include/os_compat.h @@ -0,0 +1,43 @@ +/* + * ALSA lib - compatibility header for supporting various OSes + * Copyright (C) 2022 by Takayoshi SASANO + * + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __OS_COMPAT_H +#define __OS_COMPAT_H + +#ifndef ESTRPIPE +#define ESTRPIPE ESPIPE +#endif + +#ifndef ERESTART +#define ERESTART EINTR +#endif + +#ifndef SCHED_IDLE +#define SCHED_IDLE SCHED_OTHER +#endif + +#if defined(__OpenBSD__) +/* these functions in are not implemented */ +#define sched_getparam(pid, param) (-1) +#define sched_setscheduler(pid, policy, param) (-1) +#endif + +#endif diff --git a/include/version.h b/include/version.h new file mode 100644 index 0000000..506bbca --- /dev/null +++ b/include/version.h @@ -0,0 +1,9 @@ +/* + * version.h + */ + +#define SND_UTIL_MAJOR 1 +#define SND_UTIL_MINOR 2 +#define SND_UTIL_SUBMINOR 14 +#define SND_UTIL_VERSION ((SND_UTIL_MAJOR<<16)|(SND_UTIL_MINOR<<8)|SND_UTIL_SUBMINOR) +#define SND_UTIL_VERSION_STR "1.2.14" diff --git a/nhlt/Makefile.am b/nhlt/Makefile.am new file mode 100644 index 0000000..5aadda7 --- /dev/null +++ b/nhlt/Makefile.am @@ -0,0 +1,6 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include + +bin_PROGRAMS = nhlt-dmic-info +nhlt_dmic_info_SOURCES = nhlt-dmic-info.c +man_MANS = nhlt-dmic-info.1 +EXTRA_DIST = nhlt-dmic-info.1 diff --git a/nhlt/nhlt-dmic-info.1 b/nhlt/nhlt-dmic-info.1 new file mode 100644 index 0000000..22fdc5a --- /dev/null +++ b/nhlt/nhlt-dmic-info.1 @@ -0,0 +1,37 @@ +.TH NHLT-DMIC-INFO 1 "16 May 2023" +.SH NAME +nhlt-dmic-info \- dump microphone array information from ACPI NHLT table +.SH SYNOPSIS +\fBnhlt-dmic-info\fP [\fI\-option\fP] +.SH DESCRIPTION + +\fB\fBnhlt-dmic-info\fP\fP dumps microphone array information from ACPI NHLT +table in JSON format. + +.SH OPTIONS + +.TP +\fI\-h\fP | \fI\-\-help\fP + +Prints the help information. + +.TP +\fI\-f \fP | \fI\-\-file=\fP + +Input file with the binary ACPI NHLT table (default is \fB/sys/firmware/acpi/tables/NHLT\fR). + +.TP +\fI\-o \fP | \fI\-\-output=\fP + +JSON output file (default is stdout: \fB\-\fR). + +.SH EXAMPLES +.nf +\fBnhlt-dmic-info \-f nhlt.bin \-o dmic.json\fR + +.ne +.SH BUGS +None known. +.SH AUTHOR +\fBnhlt-dmic-info\fP is by Jaroslav Kysela . +This document is by Jaroslav Kysela . diff --git a/nhlt/nhlt-dmic-info.c b/nhlt/nhlt-dmic-info.c new file mode 100644 index 0000000..7726c3c --- /dev/null +++ b/nhlt/nhlt-dmic-info.c @@ -0,0 +1,427 @@ +/* + * Extract microphone configuration from the ACPI NHLT table + * + * Specification: + * https://01.org/sites/default/files/595976_intel_sst_nhlt.pdf + * + * Author: Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int debug = 0; + +/* + * Dump dmic parameters in json + */ + +#define ACPI_HDR_SIZE (4 + 4 + 1 + 1 + 6 + 8 + 4 + 4 + 4) +#define NHLT_EP_HDR_SIZE (4 + 1 + 1 + 2 + 2 + 2 + 4 + 1 + 1 + 1) +#define VENDOR_MIC_CFG_SIZE (1 + 1 + 2 + 2 + 2 + 1 + 1 + 2 + 2 + 2 + 2 + 2 + 2) + +static const char *microphone_type(uint8_t type) +{ + switch (type) { + case 0: return "omnidirectional"; + case 1: return "subcardoid"; + case 2: return "cardoid"; + case 3: return "supercardoid"; + case 4: return "hypercardoid"; + case 5: return "8shaped"; + case 7: return "vendor"; + } + return "unknown"; +} + +static const char *microphone_location(uint8_t location) +{ + switch (location) { + case 0: return "laptop-top-panel"; + case 1: return "laptop-bottom-panel"; + case 2: return "laptop-left-panel"; + case 3: return "laptop-right-panel"; + case 4: return "laptop-front-panel"; + case 5: return "laptop-rear-panel"; + } + return "unknown"; +} + + +static inline uint8_t get_u8(uint8_t *base, uint32_t off) +{ + return *(base + off); +} + +static inline int32_t get_s16le(uint8_t *base, uint32_t off) +{ + uint32_t v = *(base + off + 0) | + (*(base + off + 1) << 8); + if (v & 0x8000) + return -((int32_t)0x10000 - (int32_t)v); + return v; +} + +static inline uint32_t get_u32le(uint8_t *base, uint32_t off) +{ + return *(base + off + 0) | + (*(base + off + 1) << 8) | + (*(base + off + 2) << 16) | + (*(base + off + 3) << 24); +} + +static int nhlt_dmic_config(FILE *out, uint8_t *dmic, uint8_t mic) +{ + int32_t angle_begin, angle_end; + + if (mic > 0) + fprintf(out, ",\n"); + fprintf(out, "\t\t{\n"); + fprintf(out, "\t\t\t\"channel\":%i,\n", mic); + fprintf(out, "\t\t\t\"type\":\"%s\",\n", microphone_type(get_u8(dmic, 0))); + fprintf(out, "\t\t\t\"location\":\"%s\"", microphone_location(get_u8(dmic, 1))); + if (get_s16le(dmic, 2) != 0) + fprintf(out, ",\n\t\t\t\"speaker-distance\":%i", get_s16le(dmic, 2)); + if (get_s16le(dmic, 4) != 0) + fprintf(out, ",\n\t\t\t\"horizontal-offset\":%i", get_s16le(dmic, 4)); + if (get_s16le(dmic, 6) != 0) + fprintf(out, ",\n\t\t\t\"vertical-offset\":%i", get_s16le(dmic, 6)); + if (get_u8(dmic, 8) != 0) + fprintf(out, ",\n\t\t\t\"freq-low-band\":%i", get_u8(dmic, 8) * 5); + if (get_u8(dmic, 9) != 0) + fprintf(out, ",\n\t\t\t\"freq-high-band\":%i", get_u8(dmic, 9) * 500); + if (get_s16le(dmic, 10) != 0) + fprintf(out, ",\n\t\t\t\"direction-angle\":%i", get_s16le(dmic, 10)); + if (get_s16le(dmic, 12) != 0) + fprintf(out, ",\n\t\t\t\"elevation-angle\":%i", get_s16le(dmic, 12)); + angle_begin = get_s16le(dmic, 14); + angle_end = get_s16le(dmic, 16); + if (!((angle_begin == 180 && angle_end == -180) || + (angle_begin == -180 && angle_end == 180))) { + fprintf(out, ",\n\t\t\t\"vertical-angle-begin\":%i,\n", angle_begin); + fprintf(out, "\t\t\t\"vertical-angle-end\":%i", angle_end); + } + angle_begin = get_s16le(dmic, 18); + angle_end = get_s16le(dmic, 20); + if (!((angle_begin == 180 && angle_end == -180) || + (angle_begin == -180 && angle_end == 180))) { + fprintf(out, ",\n\t\t\t\"horizontal-angle-begin\":%i,\n", angle_begin); + fprintf(out, "\t\t\t\"horizontal-angle-end\":%i", angle_end); + } + fprintf(out, "\n\t\t}"); + return 0; +} + +static int nhlt_dmic_ep_to_json(FILE *out, uint8_t *ep, uint32_t ep_size) +{ + uint32_t off, specific_cfg_size; + uint8_t config_type, array_type, mic, num_mics; + int res; + + off = NHLT_EP_HDR_SIZE; + specific_cfg_size = get_u32le(ep, off); + if (off + specific_cfg_size > ep_size) + goto oob; + off += 4; + config_type = get_u8(ep, off + 1); + if (config_type != 1) /* mic array */ + return 0; + array_type = get_u8(ep, off + 2); + if ((array_type & 0x0f) != 0x0f) { + fprintf(stderr, "Unsupported ArrayType %02x\n", array_type & 0x0f); + return -EINVAL; + } + num_mics = get_u8(ep, off + 3); + fprintf(out, "{\n"); + fprintf(out, "\t\"mics-data-version\":1,\n"); + fprintf(out, "\t\"mics-data-source\":\"acpi-nhlt\""); + for (mic = 0; mic < num_mics; mic++) { + if (off - NHLT_EP_HDR_SIZE + VENDOR_MIC_CFG_SIZE > specific_cfg_size) { + fprintf(out, "\n}\n"); + goto oob; + } + if (mic == 0) + fprintf(out, ",\n\t\"mics\":[\n"); + res = nhlt_dmic_config(out, ep + off + 4, mic); + if (res < 0) + return res; + off += VENDOR_MIC_CFG_SIZE; + } + if (num_mics > 0) + fprintf(out, "\n\t]\n"); + fprintf(out, "}\n"); + return num_mics; +oob: + fprintf(stderr, "Data (out-of-bounds) error\n"); + return -EINVAL; +} + +static int nhlt_table_to_json(FILE *out, uint8_t *nhlt, uint32_t size) +{ + uint32_t _size, off, ep_size; + uint8_t sum = 0, ep, ep_count, link_type, dmics = 0; + int res; + + _size = get_u32le(nhlt, 4); + if (_size != size) { + fprintf(stderr, "Table size mismatch (%08x != %08x)\n", _size, (uint32_t)size); + return -EINVAL; + } + for (off = 0; off < size; off++) + sum += get_u8(nhlt, off); + if (sum != 0) { + fprintf(stderr, "Checksum error (%02x)\n", sum); + return -EINVAL; + } + /* skip header */ + off = ACPI_HDR_SIZE; + ep_count = get_u8(nhlt, off++); + for (ep = 0; ep < ep_count; ep++) { + if (off + 17 > size) + goto oob; + ep_size = get_u32le(nhlt, off); + if (off + ep_size > size) + goto oob; + link_type = get_u8(nhlt, off + 4); + res = 0; + if (link_type == 2) { /* PDM */ + res = nhlt_dmic_ep_to_json(out, nhlt + off, ep_size); + if (res > 0) + dmics++; + } + if (res < 0) + return res; + off += ep_size; + } + if (dmics == 0) { + fprintf(stderr, "No dmic endpoint found\n"); + return -EINVAL; + } + return 0; +oob: + fprintf(stderr, "Data (out-of-bounds) error\n"); + return -EINVAL; +} + +static int nhlt_to_json(FILE *out, const char *nhlt_file) +{ + struct stat st; + uint8_t *buf; + int _errno, fd, res; + size_t pos, size; + ssize_t ret; + + if (stat(nhlt_file, &st)) + return -errno; + size = st.st_size; + if (size < 45) + return -EINVAL; + buf = malloc(size); + if (buf == NULL) + return -ENOMEM; + fd = open(nhlt_file, O_RDONLY); + if (fd < 0) { + _errno = errno; + fprintf(stderr, "Unable to open file '%s': %s\n", nhlt_file, strerror(errno)); + free(buf); + return _errno; + } + pos = 0; + while (pos < size) { + ret = read(fd, buf + pos, size - pos); + if (ret <= 0) { + fprintf(stderr, "Short read\n"); + close(fd); + free(buf); + return -EIO; + } + pos += ret; + } + close(fd); + res = nhlt_table_to_json(out, buf, size); + free(buf); + return res; +} + +/* + * + */ + +#define PROG "nhlt-dmic-info" +#define PROG_VERSION "1" + +#define NHLT_FILE "/sys/firmware/acpi/tables/NHLT" + +#define TITLE 0x0100 +#define HEADER 0x0200 +#define FILEARG 0x0400 + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + +struct arg { + int sarg; + char *larg; + char *comment; +}; + +static struct arg args[] = { +{ TITLE, NULL, "Usage: nhtl-dmic-json " }, +{ HEADER, NULL, "global options:" }, +{ 'h', "help", "this help" }, +{ 'v', "version", "print version of this program" }, +{ FILEARG | 'f', "file", "NHLT file (default " NHLT_FILE ")" }, +{ FILEARG | 'o', "output", "output file" }, +{ 0, NULL, NULL } +}; + +static void help(void) +{ + struct arg *n = args, *a; + char *larg, sa[4], buf[32]; + int sarg; + + sa[0] = '-'; + sa[2] = ','; + sa[3] = '\0'; + while (n->comment) { + a = n; + n++; + sarg = a->sarg; + if (sarg & (HEADER|TITLE)) { + printf("%s%s\n", (sarg & HEADER) != 0 ? "\n" : "", + a->comment); + continue; + } + buf[0] = '\0'; + larg = a->larg; + sa[1] = a->sarg; + sprintf(buf, "%s%s%s", sa[1] ? sa : "", + larg ? "--" : "", larg ? larg : ""); + if (sarg & FILEARG) + strcat(buf, " #"); + printf(" %-15s %s\n", buf, a->comment); + } +} + +int main(int argc, char *argv[]) +{ + char *nhlt_file = NHLT_FILE; + char *output_file = "-"; + int i, j, k, res; + struct arg *a; + struct option *o, *long_option; + char *short_option; + FILE *output = NULL; + + long_option = calloc(ARRAY_SIZE(args), sizeof(struct option)); + if (long_option == NULL) + exit(EXIT_FAILURE); + short_option = malloc(128); + if (short_option == NULL) { + free(long_option); + exit(EXIT_FAILURE); + } + for (i = j = k = 0; i < (int)ARRAY_SIZE(args); i++) { + a = &args[i]; + if ((a->sarg & 0xff) == 0) + continue; + o = &long_option[j]; + o->name = a->larg; + o->has_arg = (a->sarg & FILEARG) != 0; + o->flag = NULL; + o->val = a->sarg & 0xff; + j++; + short_option[k++] = o->val; + if (o->has_arg) + short_option[k++] = ':'; + } + short_option[k] = '\0'; + while (1) { + int c; + + if ((c = getopt_long(argc, argv, short_option, long_option, + NULL)) < 0) + break; + switch (c) { + case 'h': + help(); + res = EXIT_SUCCESS; + goto out; + case 'f': + nhlt_file = optarg; + break; + case 'o': + output_file = optarg; + break; + case 'd': + debug = 1; + break; + case 'v': + printf(PROG " version " PROG_VERSION "\n"); + res = EXIT_SUCCESS; + goto out; + case '?': // error msg already printed + help(); + res = EXIT_FAILURE; + goto out; + default: // should never happen + fprintf(stderr, + "Invalid option '%c' (%d) not handled??\n", c, c); + } + } + free(short_option); + short_option = NULL; + free(long_option); + long_option = NULL; + + if (strcmp(output_file, "-") == 0) { + output = stdout; + } else { + output = fopen(output_file, "w+"); + if (output == NULL) { + fprintf(stderr, "Unable to create output file \"%s\": %s\n", + output_file, strerror(errno)); + res = EXIT_FAILURE; + goto out; + } + } + + if (argc - optind > 0) + fprintf(stderr, PROG ": Ignoring extra parameters\n"); + + res = 0; + if (nhlt_to_json(output, nhlt_file)) + res = EXIT_FAILURE; + +out: + if (output) + fclose(output); + free(short_option); + free(long_option); + return res; +} diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..d336b9a --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1 @@ +de eu fr ja ka ko sk diff --git a/po/Makevars b/po/Makevars new file mode 100644 index 0000000..672fef2 --- /dev/null +++ b/po/Makevars @@ -0,0 +1,78 @@ +# Makefile variables for PO directory in any package using GNU gettext. + +# Usually the message domain is the same as the package name. +DOMAIN = $(PACKAGE) + +# These two variables depend on the location of this directory. +subdir = po +top_builddir = .. + +# These options get passed to xgettext. +XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ + +# This tells whether or not to prepend "GNU " prefix to the package +# name that gets inserted into the header of the $(DOMAIN).pot file. +# Possible values are "yes", "no", or empty. If it is empty, try to +# detect it automatically by scanning the files in $(top_srcdir) for +# "GNU packagename" string. +PACKAGE_GNU = + +# This is the copyright holder that gets inserted into the header of the +# $(DOMAIN).pot file. Set this to the copyright holder of the surrounding +# package. (Note that the msgstr strings, extracted from the package's +# sources, belong to the copyright holder of the package.) Translators are +# expected to transfer the copyright for their translations to this person +# or entity, or to disclaim their copyright. The empty string stands for +# the public domain; in this case the translators are expected to disclaim +# their copyright. +COPYRIGHT_HOLDER = The ALSA Team + +# This is the email address or URL to which the translators shall report +# bugs in the untranslated strings: +# - Strings which are not entire sentences, see the maintainer guidelines +# in the GNU gettext documentation, section 'Preparing Strings'. +# - Strings which use unclear terms or require additional context to be +# understood. +# - Strings which make invalid assumptions about notation of date, time or +# money. +# - Pluralisation problems. +# - Incorrect English spelling. +# - Incorrect formatting. +# It can be your email address, or a mailing list address where translators +# can write to without being subscribed, or the URL of a web page through +# which the translators can contact you. +MSGID_BUGS_ADDRESS = + +# This is the list of locale categories, beyond LC_MESSAGES, for which the +# message catalogs shall be used. It is usually empty. +EXTRA_LOCALE_CATEGORIES = + +# This tells whether the $(DOMAIN).pot file contains messages with an 'msgctxt' +# context. Possible values are "yes" and "no". Set this to yes if the +# package uses functions taking also a message context, like pgettext(), or +# if in $(XGETTEXT_OPTIONS) you define keywords with a context argument. +USE_MSGCTXT = no + +# These options get passed to msgmerge. +# Useful options are in particular: +# --previous to keep previous msgids of translated messages, +# --quiet to reduce the verbosity. +MSGMERGE_OPTIONS = + +# These options get passed to msginit. +# If you want to disable line wrapping when writing PO files, add +# --no-wrap to MSGMERGE_OPTIONS, XGETTEXT_OPTIONS, and +# MSGINIT_OPTIONS. +MSGINIT_OPTIONS = + +# This tells whether or not to regenerate a PO file when $(DOMAIN).pot +# has changed. Possible values are "yes" and "no". Set this to no if +# the POT file is checked in the repository and the version control +# program ignores timestamps. +PO_DEPENDS_ON_POT = yes + +# This tells whether or not to forcibly update $(DOMAIN).pot and +# regenerate PO files on "make dist". Possible values are "yes" and +# "no". Set this to no if the POT file and PO files are maintained +# externally. +DIST_DEPENDS_ON_UPDATE_PO = yes diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..3d1b8ea --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,12 @@ +alsamixer/card_select.c +alsamixer/cli.c +alsamixer/device_name.c +alsamixer/die.c +alsamixer/mixer_display.c +alsamixer/mixer_widget.c +alsamixer/proc_files.c +alsamixer/textbox.c +aplay/aplay.c +seq/aconnect/aconnect.c +seq/aseqnet/aseqnet.c +speaker-test/speaker-test.c diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..a3ac111 --- /dev/null +++ b/po/de.po @@ -0,0 +1,1695 @@ +# German translations for the alsa-utils package. +# Copyright (C) 2009 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Clemens Ladisch , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.23\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-10 09:07+0100\n" +"PO-Revision-Date: 2010-11-09 21:12+0100\n" +"Last-Translator: Clemens Ladisch \n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: alsamixer/card_select.c:126 alsamixer/device_name.c:126 +msgid "Sound Card" +msgstr "Soundkarte" + +#: alsamixer/card_select.c:181 +msgid "(default)" +msgstr "(Standard)" + +#: alsamixer/card_select.c:191 +msgid "cannot enumerate sound cards" +msgstr "Fehler beim Aufzählen der Soundkarten" + +#: alsamixer/card_select.c:215 +msgid "enter device name..." +msgstr "Gerätenamen eingeben..." + +#: alsamixer/cli.c:40 +msgid "Usage: alsamixer [options]" +msgstr "Verwendung: alsamixer [Optionen]" + +#: alsamixer/cli.c:41 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"Optionen:\n" +" -h, --help Hilfe\n" +" -c, --card=NUMMER Soundkarten-Nummer oder -ID\n" +" -D, --device=NAME Mixer-Gerätename\n" +" -V, --view=MODUS Ansicht beim Starten: playback=Wiedergabe, " +"capture=Aufnahme, all=alle" + +#: alsamixer/cli.c:46 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"Debugging-Optionen:\n" +" -g, --no-color keine Farben\n" +" -a, --abstraction=NAME Mixer-Abstraktion: none/basic" + +#: alsamixer/cli.c:77 +#, c-format +msgid "invalid card index: %s\n" +msgstr "ungültige Karten-Nummer: %s\n" + +#: alsamixer/cli.c:103 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "unbekannte Abstraktion: %s\n" + +#: alsamixer/cli.c:108 +#, c-format +msgid "unknown option: %c\n" +msgstr "unbekannte Option: %c\n" + +#: alsamixer/cli.c:110 +msgid "try `alsamixer --help' for more information\n" +msgstr "siehe `alsamixer --help' für mehr Informationen\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "Gerätename:" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "%s: %s\n" + +#: alsamixer/mixer_display.c:98 +msgid "Card:" +msgstr "Gerät:" + +#: alsamixer/mixer_display.c:99 +msgid "Chip:" +msgstr "Chip:" + +#: alsamixer/mixer_display.c:100 +msgid "View:" +msgstr "Ansicht:" + +#: alsamixer/mixer_display.c:101 +msgid "Item:" +msgstr "Element:" + +#: alsamixer/mixer_display.c:104 +msgid "F1: Help" +msgstr "F1: Hilfe" + +#: alsamixer/mixer_display.c:105 +msgid "F2: System information" +msgstr "F2: System-Informationen" + +#: alsamixer/mixer_display.c:106 +msgid "F6: Select sound card" +msgstr "F6: Soundkarte auswählen" + +#: alsamixer/mixer_display.c:107 +msgid "Esc: Exit" +msgstr "Esc: Beenden" + +#: alsamixer/mixer_display.c:174 +msgid "(unplugged)" +msgstr "(entfernt)" + +#: alsamixer/mixer_display.c:192 +msgid "Playback" +msgstr "Wiedergabe" + +#: alsamixer/mixer_display.c:193 +msgid "Capture" +msgstr "Aufnahme" + +#: alsamixer/mixer_display.c:194 +msgid "All" +msgstr "Alle" + +#: alsamixer/mixer_display.c:234 +msgid "mute" +msgstr "stumm" + +#: alsamixer/mixer_display.c:275 alsamixer/mixer_display.c:285 +msgid "dB gain:" +msgstr "dB-Änderung:" + +#: alsamixer/mixer_display.c:285 +#, c-format +msgid " [%s %s, %s]" +msgstr " [%s %s; %s]" + +#: alsamixer/mixer_display.c:294 alsamixer/mixer_display.c:300 +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:312 +msgid "Off" +msgstr "Aus" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:312 +msgid "On" +msgstr "An" + +#: alsamixer/mixer_display.c:363 +msgid "The sound device was unplugged." +msgstr "Das Gerät wurde entfernt." + +#: alsamixer/mixer_display.c:364 +msgid "Press F6 to select another sound card." +msgstr "Drücken Sie F6, um eine andere Soundkarte auszuwählen." + +#: alsamixer/mixer_display.c:379 +msgid "This sound device does not have any playback controls." +msgstr "Dieses Gerät hat keine Wiedergabe-Regler." + +#: alsamixer/mixer_display.c:381 +msgid "This sound device does not have any capture controls." +msgstr "Dieses Gerät hat keine Aufnahme-Regler." + +#: alsamixer/mixer_display.c:383 +msgid "This sound device does not have any controls." +msgstr "Dieses Gerät hat keine Regler." + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:516 alsamixer/mixer_display.c:521 +msgid "O" +msgstr "O" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:518 alsamixer/mixer_display.c:522 +msgid "M" +msgstr "M" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:536 +msgid "L" +msgstr "L" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:540 +msgid "R" +msgstr "R" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:542 +msgid "CAPTURE" +msgstr "AUFNAHME" + +#: alsamixer/mixer_display.c:592 +msgid "Front" +msgstr "Vorne" + +#: alsamixer/mixer_display.c:595 +msgid "Rear" +msgstr "Hinten" + +#: alsamixer/mixer_display.c:598 speaker-test/speaker-test.c:125 +msgid "Center" +msgstr "Mitte" + +#: alsamixer/mixer_display.c:601 +msgid "Woofer" +msgstr "Bass" + +#: alsamixer/mixer_display.c:604 +msgid "Side" +msgstr "Seiten" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "Fehler beim Öffen des Mixer-Gerätes" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "Fehler beim Laden der Mixer-Regler" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "Fehler beim Öffnen des Mixer-Gerätes '%s'." + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc Beenden" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H Hilfe" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / System-Informationen" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 Ansicht Wiedergabe-Regler" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 Ansicht Aufnahme-Regler" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 Ansicht alle Regler" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab Ansichts-Modus umschalten (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F6 S Soundkarte auswählen" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L Bildschirm neu darstellen" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "Links gehe zum vorherigen Regler" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "Rechts gehe zum nächsten Regler" + +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "Oben/Unten Lautstärke ändern" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - Lautstärke ändern" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "Bild ^/v Lautstärke in großen Schritten ändern" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "Ende Lautstärke auf 0% setzen" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 Lautstärke auf 0%-90% setzen" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E linke/beide/rechte Lautstärke erhöhen" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Y X C linke/beide/rechte Lautstärke verringern" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B linke und rechte Lautstärke angleichen" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M stumm umschalten" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr ", . stumm links/rechts umschalten" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "Leertaste Aufnahme umschalten" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "Einfg Entf Aufnahme links/rechts umschalten" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "Autoren:" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr " Tim Janik" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr " Jaroslav Kysela " + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr " Clemens Ladisch " + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "Hilfe" + +#: alsamixer/proc_files.c:103 +msgid "Select File" +msgstr "Datei wählen" + +#: alsamixer/textbox.c:52 alsamixer/textbox.c:66 +msgid "Error" +msgstr "Fehler" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "Fehler beim Öffnen der Datei \"%s\"." + +#: aplay/aplay.c:178 +msgid "raw data" +msgstr "Rohdaten" + +#: aplay/aplay.c:179 +msgid "VOC" +msgstr "VOC" + +#: aplay/aplay.c:181 +msgid "WAVE" +msgstr "WAVE" + +#: aplay/aplay.c:182 +msgid "Sparc Audio" +msgstr "Sparc-Audio" + +#: aplay/aplay.c:203 +#, fuzzy, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"Verwendung: %s [Option]... [Datei]...\n" +"\n" +"-h, --help Hilfe\n" +" --version Version anzeigen\n" +"-l, --list-devices alle Soundkarten und -Geräte auflisten\n" +"-L, --list-pcms ALSA-Gerätenamen auflisten\n" +"-D, --device=NAME PCM-Gerät wählen\n" +"-q, --quiet weniger Programmausgaben\n" +"-t, --file-type TYP Dateityp (voc, wav, raw oder au)\n" +"-c, --channels=# Kanäle\n" +"-f, --format=FORMAT Sample-Format\n" +"-r, --rate=# Sample-Rate\n" +"-d, --duration=# Beenden nach # Sekunden\n" +"-M, --mmap mmap-Modus\n" +"-N, --nonblock nonblocking-Modus\n" +"-F, --period-time=# Abstand zwischen Interrupts ist # µs\n" +"-B, --buffer-time=# Puffer-Länge ist # µs\n" +" --period-size=# Abstand zwischen Interrupts ist # Frames\n" +" --buffer-size=# Puffer-Länge ist # Frames\n" +"-A, --avail-min=# freier Pufferspeicher für Wakeup ist # µs\n" +"-R, --start-delay=# Puffer-Füllgrad zum automatischen PCM-Start, in µs\n" +" (wenn <= 0, freier Puffer-Speicher)\n" +"-T, --stop-delay=# Zeit vor xrun zum automatischen PCM-Stop, in µs\n" +"-v, --verbose zeige PCM-Struktur und -Konfiguration (akkumulativ)\n" +"-V, --vumeter=TYP VU-Anzeige (TYP: mono oder stereo)\n" +"-I, --separate-channels eine Datei pro Kanal\n" +" --disable-resample keine automatische Sample-Rate-Anpassung\n" +" --disable-channels keine automatische Kanal-Anzahl-Anpassung\n" +" --disable-format keine automatische Format-Anpassung\n" +" --disable-softvol kein Software-Lautstärke-Regler (softvol)\n" +" --test-position überprüfe Position im Ring-Puffer\n" +" --test-coef=# Test-Koeffizient für Positionsprüfung (Standard 8)\n" +" Formel für Prüfung ist: Koeffizient * (Puffergröße / " +"2)\n" +" --test-nowait kein Warten auf Ringpuffer; beansprucht volle CPU-" +"Leistung\n" +" --max-file-time=# erzeuge mehrere Dateien; Wechsel nach # Sekunden\n" +" --process-id-file=# schreibe Prozess-ID in diese Datei\n" +" --use-strftime formatiere Dateiname mit strftime; %%v=Dateinummer\n" + +#: aplay/aplay.c:248 speaker-test/speaker-test.c:1023 +#, c-format +msgid "Recognized sample formats are:" +msgstr "Unterstützte Sample-Formate:" + +#: aplay/aplay.c:254 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"Nicht alle davon sind auf jeder Hardware verfügbar.\n" + +#: aplay/aplay.c:255 +#, fuzzy, c-format +msgid "The available format shortcuts are:\n" +msgstr "Unterstütze Format-Abkürzungen:\n" + +#: aplay/aplay.c:256 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16 Bits, Little Endian, 44100 Hz, stereo)\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16 Bits, Big Endian, 44100 Hz, stereo)\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16 Bits, Little Endian, 48000 Hz, stereo)\n" + +#: aplay/aplay.c:272 +msgid "no soundcards found..." +msgstr "keine Soundkarten gefunden ..." + +#: aplay/aplay.c:275 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** Liste der Hardware-Geräte (%s) ****\n" + +#: aplay/aplay.c:304 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "Karte %i: %s [%s], Gerät %i: %s [%s]\n" + +#: aplay/aplay.c:310 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " Sub-Geräte: %i/%i\n" + +#: aplay/aplay.c:317 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " Sub-Gerät #%i: %s\n" + +#: aplay/aplay.c:398 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "Abbruch durch Signal %s ...\n" + +#: aplay/aplay.c:553 +msgid "command should be named either arecord or aplay" +msgstr "Befehl sollte arecord oder aplay sein" + +#: aplay/aplay.c:597 +#, c-format +msgid "unrecognized file format %s" +msgstr "unbekanntes Dateiformat %s" + +#: aplay/aplay.c:604 +#, fuzzy, c-format +msgid "invalid channels argument '%s'" +msgstr "ungültige Sender-Adresse %s\n" + +#: aplay/aplay.c:608 +#, c-format +msgid "value %i for channels is invalid" +msgstr "Kanalanzahl %i ist ungültig" + +#: aplay/aplay.c:627 +#, c-format +msgid "wrong extended format '%s'" +msgstr "erweitertes Format '%s' ist ungültig" + +#: aplay/aplay.c:635 +#, fuzzy, c-format +msgid "invalid rate argument '%s'" +msgstr "ungültige Quell-Adresse %s\n" + +#: aplay/aplay.c:642 +#, c-format +msgid "bad speed value %i" +msgstr "ungültige Rate %i" + +#: aplay/aplay.c:648 +msgid "duration and samples arguments cannot be used together" +msgstr "" + +#: aplay/aplay.c:653 +#, fuzzy, c-format +msgid "invalid duration argument '%s'" +msgstr "ungültige Ziel-Adresse %s\n" + +#: aplay/aplay.c:660 +msgid "samples and duration arguments cannot be used together" +msgstr "" + +#: aplay/aplay.c:665 +#, fuzzy, c-format +msgid "invalid samples argument '%s'" +msgstr "ungültige Sender-Adresse %s\n" + +#: aplay/aplay.c:677 +#, fuzzy, c-format +msgid "invalid period time argument '%s'" +msgstr "ungültige Sender-Adresse %s\n" + +#: aplay/aplay.c:684 +#, fuzzy, c-format +msgid "invalid buffer time argument '%s'" +msgstr "ungültige Quell-Adresse %s\n" + +#: aplay/aplay.c:691 +#, fuzzy, c-format +msgid "invalid period size argument '%s'" +msgstr "ungültige Sender-Adresse %s\n" + +#: aplay/aplay.c:698 +#, fuzzy, c-format +msgid "invalid buffer size argument '%s'" +msgstr "ungültige Quell-Adresse %s\n" + +#: aplay/aplay.c:705 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "" + +#: aplay/aplay.c:712 +#, fuzzy, c-format +msgid "invalid start delay argument '%s'" +msgstr "ungültige Sender-Adresse %s\n" + +#: aplay/aplay.c:719 +#, fuzzy, c-format +msgid "invalid stop delay argument '%s'" +msgstr "ungültige Sender-Adresse %s\n" + +#: aplay/aplay.c:774 +#, fuzzy, c-format +msgid "invalid test coef argument '%s'" +msgstr "ungültige Quell-Adresse %s\n" + +#: aplay/aplay.c:786 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "" + +#: aplay/aplay.c:806 speaker-test/speaker-test.c:666 +#, fuzzy, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "Fehler beim Setzen des Mindest-verfügbar-Wertes: %s\n" + +#: aplay/aplay.c:812 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "Siehe `%s --help' für mehr Informationen.\n" + +#: aplay/aplay.c:828 +#, c-format +msgid "audio open error: %s" +msgstr "Fehler beim Öffnen des Gerätes: %s" + +#: aplay/aplay.c:833 +#, c-format +msgid "info error: %s" +msgstr "Fehler beim Lesen der Geräteinformationen: %s" + +#: aplay/aplay.c:840 +#, c-format +msgid "nonblock setting error: %s" +msgstr "Fehler beim Setzen des nonblock-Modus: %s" + +#: aplay/aplay.c:850 aplay/aplay.c:974 aplay/aplay.c:1264 aplay/aplay.c:1458 +#: aplay/aplay.c:1979 aplay/aplay.c:2009 +msgid "not enough memory" +msgstr "nicht genug Speicher" + +#: aplay/aplay.c:874 +#, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "Fehler beim Schreiben der Prozess-ID-Datei %s: %s" + +#: aplay/aplay.c:964 +#, c-format +msgid "read error (called from line %i)" +msgstr "Lesefehler (aufgerufen von Zeile %i)" + +#: aplay/aplay.c:1031 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"unbekannte Länge des 'fmt '-Blocks (gelesen: %u, sollte mindestens %u sein)" + +#: aplay/aplay.c:1042 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"unbekannte Länge des erweiterten 'fmt '-Blocks (gelesen: %u, sollte " +"mindestens %u sein)" + +#: aplay/aplay.c:1047 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "ungültiger Format-Wert im erweiterten 'fmt '-Block" + +#: aplay/aplay.c:1054 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "" +"kann WAVE-Datei-Format 0x%04x nicht abspielen; ist weder PCM noch FLOAT" + +#: aplay/aplay.c:1059 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "kann WAVE-Datei mit %d Kanälen nicht abspielen" + +#: aplay/aplay.c:1067 aplay/aplay.c:1193 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "Warnung: benutztes Format ist U8\n" + +#: aplay/aplay.c:1077 aplay/aplay.c:1090 aplay/aplay.c:1101 +#, fuzzy, c-format +msgid "Warning: format is changed to %s\n" +msgstr "Warnung: benutztes Format ist U8\n" + +#: aplay/aplay.c:1106 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr "" +"kann WAVE-Datei mit %d-Bit-Samples in %d Bytes (%d Kanäle) nicht abspielen" + +#: aplay/aplay.c:1129 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr "kann WAVE-Datei mit %d-Bit-Samples nicht abspielen" + +#: aplay/aplay.c:1187 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "Warnung: benutztes Format ist MU_LAW\n" + +#: aplay/aplay.c:1199 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "Warnung: benutztes Format ist S16_BE\n" + +#: aplay/aplay.c:1212 aplay/aplay.c:2294 aplay/aplay.c:2301 aplay/aplay.c:2831 +msgid "read error" +msgstr "Lesefehler" + +#: aplay/aplay.c:1242 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "" + +#: aplay/aplay.c:1251 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "" + +#: aplay/aplay.c:1284 +#, c-format +msgid "Channel %d doesn't match with hw_parmas" +msgstr "" + +#: aplay/aplay.c:1310 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "" +"ungültige Konfiguration für dieses Gerät: keine unterstützte Konfiguration" + +#: aplay/aplay.c:1314 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "" + +#: aplay/aplay.c:1334 +msgid "Access type not available" +msgstr "Zugriffs-Modus nicht unterstützt" + +#: aplay/aplay.c:1339 +msgid "Sample format non available" +msgstr "Sample-Format nicht unterstützt" + +#: aplay/aplay.c:1345 +msgid "Channels count non available" +msgstr "Kanalanzahl nicht unterstützt" + +#: aplay/aplay.c:1360 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "" +"Warnung: Rate ist nicht exakt (angefordert: %i Hz, unterstützt: %i Hz)\n" + +#: aplay/aplay.c:1366 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " probieren Sie bitte das plug-Plugin: %s\n" + +#: aplay/aplay.c:1403 +msgid "Unable to install hw params:" +msgstr "Fehler beim Setzen der Hardware-Parameter:" + +#: aplay/aplay.c:1410 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "Periode gleich der Puffer-Größe wird nicht unterstützt (%lu == %lu)" + +#: aplay/aplay.c:1441 +msgid "unable to install sw params:" +msgstr "Fehler beim Setzen der Software-Parameter:" + +#: aplay/aplay.c:1476 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "Fehler bei snd_pcm_mmap_begin: %s" + +#: aplay/aplay.c:1505 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "Fehler beim Setzen von O_NONBLOCK in stdin\n" + +#: aplay/aplay.c:1529 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "\rPause-Kommando ignoriert (keine HW-Unterstützung)\n" + +#: aplay/aplay.c:1537 +#, c-format +msgid "pause push error: %s" +msgstr "Fehler beim Pausieren: %s" + +#: aplay/aplay.c:1548 +#, c-format +msgid "pause release error: %s" +msgstr "Fehler beim Beenden der Pause: %s" + +#: aplay/aplay.c:1564 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" +"\r=== PAUSE === " + +#: aplay/aplay.c:1606 +#, c-format +msgid "status error: %s" +msgstr "Status-Fehler: %s" + +#: aplay/aplay.c:1611 +#, fuzzy, c-format +msgid "fatal %s: %s" +msgstr "%s: %s\n" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1626 aplay/aplay.c:1634 +msgid "underrun" +msgstr "Unterlauf" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1634 +msgid "overrun" +msgstr "Überlauf" + +#: aplay/aplay.c:1622 aplay/aplay.c:1633 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (mindestens %.3f ms)\n" + +#: aplay/aplay.c:1638 +#, c-format +msgid "Status:\n" +msgstr "Status:\n" + +#: aplay/aplay.c:1642 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "Unter-/Überlauf: Fehler beim Re-Initialisieren des Gerätes: %s" + +#: aplay/aplay.c:1648 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "Status (DRAINING):\n" + +#: aplay/aplay.c:1652 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "Format-Wechsel der Aufnahme-Daten? Versuche Wiederherstellung ...\n" + +#: aplay/aplay.c:1654 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "XRUN (DRAINING): Fehler beim Re-Initialisieren des Gerätes: %s" + +#: aplay/aplay.c:1661 +#, c-format +msgid "Status(R/W):\n" +msgstr "Status (R/W):\n" + +#: aplay/aplay.c:1664 +#, c-format +msgid "read/write error, state = %s" +msgstr "Lese-/Schreibfehler, Status = %s" + +#: aplay/aplay.c:1674 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "Ruhezustand. Versuche, aufzuwecken. " + +#: aplay/aplay.c:1679 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "Fehlgeschlagen. Re-Initialisierung. " + +#: aplay/aplay.c:1681 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "Ruhezustand: Fehler beim Re-Initialisieren: %s" + +#: aplay/aplay.c:1686 +#, c-format +msgid "Done.\n" +msgstr "Fertig.\n" + +#: aplay/aplay.c:1708 +#, c-format +msgid " !clip " +msgstr " !clip " + +#: aplay/aplay.c:1855 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "%d-Bit-Samples werden nicht unterstützt.\n" + +#: aplay/aplay.c:1889 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "Höchstwert (%li Samples): 0x%08x " + +#: aplay/aplay.c:1923 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "" +"verdächtige Puffer-Position (total %li): avail = %li, delay = %li, buffer = " +"%li\n" + +#: aplay/aplay.c:2051 +#, c-format +msgid "write error: %s" +msgstr "Schreibfehler: %s" + +#: aplay/aplay.c:2099 +#, c-format +msgid "writev error: %s" +msgstr "Vektor-Schreib-Fehler: %s" + +#: aplay/aplay.c:2143 +#, c-format +msgid "read error: %s" +msgstr "Lesefehler: %s" + +#: aplay/aplay.c:2187 +#, c-format +msgid "readv error: %s" +msgstr "Vektor-Lese-Fehler: %s" + +#: aplay/aplay.c:2235 +msgid "can't allocate buffer for silence" +msgstr "nicht genug Speicher für Stille-Block" + +#: aplay/aplay.c:2244 aplay/aplay.c:2472 aplay/aplay.c:2477 aplay/aplay.c:2527 +#: aplay/aplay.c:2536 aplay/aplay.c:2543 aplay/aplay.c:2553 aplay/aplay.c:2559 +#: aplay/aplay.c:2631 aplay/aplay.c:2661 aplay/aplay.c:2675 +msgid "write error" +msgstr "Schreibfehler" + +#: aplay/aplay.c:2257 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "voc_pcm_flush - Fehler in set_silence" + +#: aplay/aplay.c:2260 +msgid "voc_pcm_flush error" +msgstr "Schreibfehler" + +#: aplay/aplay.c:2285 +msgid "malloc error" +msgstr "nicht genug Speicher" + +#: aplay/aplay.c:2289 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "Spiele Creative Labs Channel-Datei '%s'...\n" + +#: aplay/aplay.c:2357 aplay/aplay.c:2452 +msgid "can't play packed .voc files" +msgstr "kann komprimierte .voc-Dateien nicht abspielen" + +#: aplay/aplay.c:2412 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "" +"kann Schleife nicht abspielen; Dateiposition in %s ist nicht änderbar\n" + +#: aplay/aplay.c:2461 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "Unbekannter Block-Typ %d. Abbruch." + +#: aplay/aplay.c:2595 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "Format %s wird in WAVE nicht unterstützt ..." + +#: aplay/aplay.c:2655 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "Format %s wird in Sparc-Audio nicht unterstützt ..." + +#: aplay/aplay.c:2736 +msgid "Playing" +msgstr "Wiedergabe:" + +#: aplay/aplay.c:2736 +msgid "Recording" +msgstr "Aufnahme:" + +#: aplay/aplay.c:2740 +#, c-format +msgid "Rate %d Hz, " +msgstr "Rate: %d Hz, " + +#: aplay/aplay.c:2742 +#, c-format +msgid "Mono" +msgstr "mono" + +#: aplay/aplay.c:2744 +#, c-format +msgid "Stereo" +msgstr "stereo" + +#: aplay/aplay.c:2746 +#, c-format +msgid "Channels %i" +msgstr "%i Kanäle" + +#: aplay/aplay.c:3344 aplay/aplay.c:3397 +#, c-format +msgid "You need to specify %d files" +msgstr "Es werden %d Dateien benötigt." + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - ALSA Sequenzer Verbindungs-Manager\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "Copyright © 1999-2000 Takashi Iwai\n" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "Verwendung:\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * Verbindung zwischen zwei Ports herstellen/trennen\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [Optionen] Sender Empfänger\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " Sender, Empfänger = Client:Port\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect Verbindung trennen\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive exklusive Verbindung\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # benutze Zeitstempel der Queue #\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # benutze Tick-Zeitstempel der Queue #\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * Ports und Verbindungen auflisten\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr " aconnect -i|-o [Optionen]\n" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr " -i,--input Eingabe-Ports (lesbar) auflisten\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr " -o,--output Ausgabe-Ports (schreibbar) auflisten\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr " -l,--list Verbindungen der Ports auflisten\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr " * alle Verbindungen trennen\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr " -x,--removeall\n" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "verbunden zu" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "verbunden von" + +#: seq/aconnect/aconnect.c:171 +#, fuzzy, c-format +msgid "client %d: '%s' [type=%s" +msgstr "Client %d: '%s' [Typ=%s]\n" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "User" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "Kernel" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "Fehler beim Öffnen des Sequenzers\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "Fehler beim Lesen der Client-ID\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "Fehler beim Setzen des Client-Namens\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "ungültige Sender-Adresse %s\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:290 +#, c-format +msgid "invalid destination address %s\n" +msgstr "ungültige Ziel-Adresse %s\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "keine Verbindung gefunden\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "Verbindungs-Trennung fehlgeschlagen (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "Verbindung ist bereits vorhanden\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "Verbindung fehlgeschlagen (%s)\n" + +#: seq/aseqnet/aseqnet.c:164 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - Netzwerk-Client/Server für ALSA Sequenzer\n" + +#: seq/aseqnet/aseqnet.c:165 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr " Copyright © 1999 Takashi Iwai\n" + +#: seq/aseqnet/aseqnet.c:166 +#, c-format +msgid "usage:\n" +msgstr "Verwendung:\n" + +#: seq/aseqnet/aseqnet.c:167 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " Server-Modus: aseqnet [Optionen]\n" + +#: seq/aseqnet/aseqnet.c:168 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " Client-Modus: aseqnet [Optionen] ServerHost\n" + +#: seq/aseqnet/aseqnet.c:169 +#, c-format +msgid "options:\n" +msgstr "Optionen:\n" + +#: seq/aseqnet/aseqnet.c:170 +#, fuzzy, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : TCP-Port (Zahl oder Service-Name)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr " -s,--source # : lese von Sequenzer-Port (Client:Port)\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest # : schreibe auf Sequenzer-Port (Client:Port)\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v,--verbose : ausführliche Meldungen\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i,--info : Ausgabe bestimmter empfangener Ereignisse\n" + +#: seq/aseqnet/aseqnet.c:188 +#, c-format +msgid "can't malloc\n" +msgstr "nicht genug Speicher\n" + +#: seq/aseqnet/aseqnet.c:213 +#, c-format +msgid "closing files..\n" +msgstr "schließe Dateien ...\n" + +#: seq/aseqnet/aseqnet.c:272 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "Sequenzer geöffnet: %d:%d\n" + +#: seq/aseqnet/aseqnet.c:279 +#, c-format +msgid "invalid source address %s\n" +msgstr "ungültige Quell-Adresse %s\n" + +#: seq/aseqnet/aseqnet.c:309 +#, c-format +msgid "service '%s' is not found in /etc/services\n" +msgstr "Service '%s' in /etc/services nicht gefunden\n" + +#: seq/aseqnet/aseqnet.c:377 +#, c-format +msgid "too many connections!\n" +msgstr "zu viele Verbindungen\n" + +#: seq/aseqnet/aseqnet.c:388 +#, c-format +msgid "accepted[%d]\n" +msgstr "angenommen[%d]\n" + +#: seq/aseqnet/aseqnet.c:411 +#, c-format +msgid "can't get address %s\n" +msgstr "kann Adresse für %s nicht bestimmen\n" + +#: seq/aseqnet/aseqnet.c:422 +#, c-format +msgid "ok.. connected\n" +msgstr "OK ... verbunden\n" + +#: seq/aseqnet/aseqnet.c:518 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "Channel %2d: Control event : %5d\n" + +#: seq/aseqnet/aseqnet.c:522 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "Channel %2d: Pitchbender : %5d\n" + +#: seq/aseqnet/aseqnet.c:526 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "Channel %2d: Note On evenet : %5d\n" + +#: seq/aseqnet/aseqnet.c:530 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "Channel %2d: Note Off event: %5d\n" + +#: seq/aseqnet/aseqnet.c:585 +#, c-format +msgid "disconnected\n" +msgstr "Verbindung getrennt\n" + +#: speaker-test/speaker-test.c:121 +msgid "Front Left" +msgstr "Vorne links" + +#: speaker-test/speaker-test.c:122 +msgid "Front Right" +msgstr "Vorne rechts" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Left" +msgstr "Hinten links" + +#: speaker-test/speaker-test.c:124 +msgid "Rear Right" +msgstr "Hinten rechts" + +#: speaker-test/speaker-test.c:126 +msgid "LFE" +msgstr "Bass" + +#: speaker-test/speaker-test.c:127 +msgid "Side Left" +msgstr "Seitlich links" + +#: speaker-test/speaker-test.c:128 +msgid "Side Right" +msgstr "Seitlich rechts" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 9" +msgstr "Kanal 9" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 10" +msgstr "Kanal 10" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 11" +msgstr "Kanal 11" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 12" +msgstr "Kanal 12" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 13" +msgstr "Kanal 13" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 14" +msgstr "Kanal 14" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 15" +msgstr "Kanal 15" + +#: speaker-test/speaker-test.c:136 +msgid "Channel 16" +msgstr "Kanal 16" + +#: speaker-test/speaker-test.c:519 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "Ungültige Konfiguration: keine unterstützte Konfiguration: %s\n" + +#: speaker-test/speaker-test.c:526 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "Zugriffsmodus nicht unterstützt: %s\n" + +#: speaker-test/speaker-test.c:533 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "Sample-Format nicht unterstützt: %s\n" + +#: speaker-test/speaker-test.c:540 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "Kanal-Anzahl %i nicht unterstützt: %s\n" + +#: speaker-test/speaker-test.c:548 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "Rate %i Hz nicht unterstützt: %s\n" + +#: speaker-test/speaker-test.c:553 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "" +"Rate ist nicht exakt (angefordert: %i Hz, unterstützt: %i Hz, Fehlercode " +"%d)\n" + +#: speaker-test/speaker-test.c:557 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "Rate ist %i Hz (angefordert: %i Hz)\n" + +#: speaker-test/speaker-test.c:563 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "Puffergröße von %lu bis %lu\n" + +#: speaker-test/speaker-test.c:564 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "Periodengröße von %lu bis %lu\n" + +#: speaker-test/speaker-test.c:566 +#, c-format +msgid "Requested period time %u us\n" +msgstr "Angeforderte Periodenzeit %u µs\n" + +#: speaker-test/speaker-test.c:569 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "Fehler beim Setzen der Periodenzeit %u µs: %s\n" + +#: speaker-test/speaker-test.c:575 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "Angeforderte Pufferlänge %u µs\n" + +#: speaker-test/speaker-test.c:578 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "Fehler beim Setzen der Pufferlänge %u µs: %s\n" + +#: speaker-test/speaker-test.c:587 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "Verwende maximale Puffergröße %lu\n" + +#: speaker-test/speaker-test.c:590 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "Fehler beim Setzen der Puffergröße %lu: %s\n" + +#: speaker-test/speaker-test.c:596 +#, c-format +msgid "Periods = %u\n" +msgstr "Perioden = %u\n" + +#: speaker-test/speaker-test.c:599 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "Fehler beim Setzen der Periodenanzahl %u: %s\n" + +#: speaker-test/speaker-test.c:608 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "Fehler beim Setzen der Hardware-Parameter: %s\n" + +#: speaker-test/speaker-test.c:614 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "gesetzt: period_size = %lu\n" + +#: speaker-test/speaker-test.c:615 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "gesetzt: buffer_size = %lu\n" + +#: speaker-test/speaker-test.c:617 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "Puffer zu klein, kann nicht benutzt werden\n" + +#: speaker-test/speaker-test.c:630 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "Fehler beim Lesen der Software-Parameter: %s\n" + +#: speaker-test/speaker-test.c:637 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "Fehler beim Setzen des Start-Schwellenwertes: %s\n" + +#: speaker-test/speaker-test.c:644 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "Fehler beim Setzen des Mindest-verfügbar-Wertes: %s\n" + +#: speaker-test/speaker-test.c:651 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "Fehler beim Setzen der Software-Parameter: %s\n" + +#: speaker-test/speaker-test.c:671 +#, fuzzy, c-format +msgid "Unable to set channel map: %s\n" +msgstr "Fehler beim Setzen des Mindest-verfügbar-Wertes: %s\n" + +#: speaker-test/speaker-test.c:696 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "" +"Fehler bei Unterlauf-Behandlung, Re-Initialisierung fehlgeschlagen: %s\n" + +#: speaker-test/speaker-test.c:707 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "" +"Fehler beim Aufwachen aus dem Ruhezustand, Re-Initialisierung " +"fehlgeschlagen: %s\n" + +#: speaker-test/speaker-test.c:771 speaker-test/speaker-test.c:1272 +#, c-format +msgid "No enough memory\n" +msgstr "Nicht genug Speicher\n" + +#: speaker-test/speaker-test.c:776 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "Kann WAV-Datei %s nicht öffnen\n" + +#: speaker-test/speaker-test.c:780 speaker-test/speaker-test.c:809 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "Ungültige WAV-Datei %s\n" + +#: speaker-test/speaker-test.c:785 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "Keine WAV-Datei: %s\n" + +#: speaker-test/speaker-test.c:789 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "Nicht unterstütztes WAV-Format %d in %s\n" + +#: speaker-test/speaker-test.c:794 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s ist keine Mono-Datei (%d Kanäle)\n" + +#: speaker-test/speaker-test.c:799 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "Sample-Rate (%d) stimmt nicht überein in %s\n" + +#: speaker-test/speaker-test.c:804 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "Nicht unterstütztes Sample-Format mit %d Bits in %s\n" + +#: speaker-test/speaker-test.c:865 +#, c-format +msgid "Undefined channel %d\n" +msgstr "Kanal %d nicht definiert\n" + +#: speaker-test/speaker-test.c:916 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "Schreibfehler: %d, %s\n" + +#: speaker-test/speaker-test.c:918 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery fehlgeschlagen: %d, %s\n" + +#: speaker-test/speaker-test.c:1004 +#, fuzzy, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"Verwendung: speaker-test [Option]...\n" +"-h,--help Hilfe\n" +"-D,--device Wiedergabe-Gerät\n" +"-r,--rate Sample-Rate in Hz\n" +"-c,--channels Anzahl der Kanäle\n" +"-f,--frequency Frequenz der Sinuswelle in Hz\n" +"-F,--format Sample-Format\n" +"-b,--buffer Ringpufferlänge in µs\n" +"-p,--period Periodenlänge in µs\n" +"-P,--nperiods Anzahl der Perioden\n" +"-t,--test pink=rosa Rauschen, sine=Sinuswelle, wav=WAV-Datei\n" +"-l,--nloops Anzahl der Wiederholungen, 0 = unendlich\n" +"-s,--speaker teste einen einzelnen Lautsprecher; 1=links, 2=rechts, usw.\n" +"-w,--wavfile benutze WAV-Datei als Testton\n" +"-W,--wavdir benutze Verzeichnis mit darin enthaltenen WAV-Dateien\n" +"-X,--force-frequency\termöglichen Frequenzen außerhalb der 30-8000Hz " +"Bereich\n" +"\n" + +#: speaker-test/speaker-test.c:1136 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "Ungültige Periodenanzahl %d\n" + +#: speaker-test/speaker-test.c:1152 speaker-test/speaker-test.c:1156 +#, c-format +msgid "Invalid test type %s\n" +msgstr "Ungültiger Test-Typ %s\n" + +#: speaker-test/speaker-test.c:1189 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "Unbekannte Options '%c'\n" + +#: speaker-test/speaker-test.c:1203 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "Ungültiger Wert für Option -s\n" + +#: speaker-test/speaker-test.c:1218 +#, c-format +msgid "Playback device is %s\n" +msgstr "Wiedergabe-Gerät ist %s\n" + +#: speaker-test/speaker-test.c:1219 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "Stream-Parameter sind %i Hz, %s, %i Kanäle\n" + +#: speaker-test/speaker-test.c:1222 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "Verwende 16 Oktaven rosa Rauschen\n" + +#: speaker-test/speaker-test.c:1225 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "Sinuswelle mit Frequenz %.4f Hz\n" + +#: speaker-test/speaker-test.c:1228 +#, c-format +msgid "WAV file(s)\n" +msgstr "WAV-Datei(en)\n" + +#: speaker-test/speaker-test.c:1238 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "Fehler beim Öffnen des Gerätes: %d, %s\n" + +#: speaker-test/speaker-test.c:1244 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "Fehler beim Setzen der Hardware-Parameter: %s\n" + +#: speaker-test/speaker-test.c:1248 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "Fehler beim Setzen der Software-Parameter: %s\n" + +#: speaker-test/speaker-test.c:1295 speaker-test/speaker-test.c:1317 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "Schreibfehler: %s\n" + +#: speaker-test/speaker-test.c:1303 +#, c-format +msgid "Time per period = %lf\n" +msgstr "Zeit pro Periode = %lf\n" + +#~ msgid "Warning: format is changed to S16_LE\n" +#~ msgstr "Warnung: benutztes Format ist S16_LE\n" + +#~ msgid "Warning: format is changed to S24_3LE\n" +#~ msgstr "Warnung: benutztes Format ist S24_3LE\n" + +#~ msgid "Warning: format is changed to S24_LE\n" +#~ msgstr "Warnung: benutztes Format ist S24_LE\n" diff --git a/po/eu.po b/po/eu.po new file mode 100644 index 0000000..0d1dea8 --- /dev/null +++ b/po/eu.po @@ -0,0 +1,1716 @@ +# Basque translation for alsa-utils +# Copyright (C) 2011 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Porrumentzio , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.23\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-10 09:07+0100\n" +"PO-Revision-Date: 2020-07-26 12:01+0200\n" +"Last-Translator: Porrumentzio \n" +"Language-Team: Librezale \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.3.1\n" + +#: alsamixer/card_select.c:126 alsamixer/device_name.c:126 +msgid "Sound Card" +msgstr "Soinu-txartela" + +#: alsamixer/card_select.c:181 +msgid "(default)" +msgstr "(lehenetsia)" + +#: alsamixer/card_select.c:191 +msgid "cannot enumerate sound cards" +msgstr "ezin dira zerrendatu soinu-txartelak" + +#: alsamixer/card_select.c:215 +msgid "enter device name..." +msgstr "sartu gailuaren izena..." + +#: alsamixer/cli.c:40 +msgid "Usage: alsamixer [options]" +msgstr "Erabilera: alsamixer [aukerak]" + +# capture --> atzemate, hartze, kaptura(tze)...? +#: alsamixer/cli.c:41 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"Aukera erabilgarriak:\n" +" -h, --help lagunta-koadro hau\n" +" -c, --card=ZENBAKIA soinu-txartelaren zenbakia edo id-a\n" +" -D, --device=IZENA nahasgailuaren izena\n" +" -V, --view=MODUA hasteko ikuspegia: erreprodukzioa/kapturatzea/" +"guztiak" + +#: alsamixer/cli.c:46 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"Arazketa aukerak:\n" +" -g, --no-color kolore gabeko interfazea\n" +" -a, --abstraction=IZENA nahasgailuaren abstrakzio maila: bat ere ez / " +"oinarrizkoa" + +#: alsamixer/cli.c:77 +#, c-format +msgid "invalid card index: %s\n" +msgstr "txartel-indize baliogabea: %s\n" + +#: alsamixer/cli.c:103 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "abstrakzio-maila ezezaguna: %s\n" + +#: alsamixer/cli.c:108 +#, c-format +msgid "unknown option: %c\n" +msgstr "aukera ezezaguna: %c\n" + +#: alsamixer/cli.c:110 +msgid "try `alsamixer --help' for more information\n" +msgstr "probatu `alsamixer --help' xehetasunetarako\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "Gailu-izena:" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "%s: %s\n" + +#: alsamixer/mixer_display.c:98 +msgid "Card:" +msgstr "Txartela:" + +#: alsamixer/mixer_display.c:99 +msgid "Chip:" +msgstr "Txipa:" + +#: alsamixer/mixer_display.c:100 +msgid "View:" +msgstr "Ikuspegia:" + +#: alsamixer/mixer_display.c:101 +msgid "Item:" +msgstr "Elementua:" + +#: alsamixer/mixer_display.c:104 +msgid "F1: Help" +msgstr "F1: Laguntza" + +#: alsamixer/mixer_display.c:105 +msgid "F2: System information" +msgstr "F2: Sistemaren informazioa" + +#: alsamixer/mixer_display.c:106 +msgid "F6: Select sound card" +msgstr "F6: Hautatu soinu-txartela" + +# Esc --> Ihes / Esc +#: alsamixer/mixer_display.c:107 +msgid "Esc: Exit" +msgstr "Esc: Irten" + +#: alsamixer/mixer_display.c:174 +msgid "(unplugged)" +msgstr "(deskonektatuta)" + +#: alsamixer/mixer_display.c:192 +msgid "Playback" +msgstr "Erreprodukzioa" + +#: alsamixer/mixer_display.c:193 +msgid "Capture" +msgstr "Kapturtzea" + +#: alsamixer/mixer_display.c:194 +msgid "All" +msgstr "Guztiak" + +#: alsamixer/mixer_display.c:234 +msgid "mute" +msgstr "mututu" + +#: alsamixer/mixer_display.c:275 alsamixer/mixer_display.c:285 +msgid "dB gain:" +msgstr "dB irabazia:" + +#: alsamixer/mixer_display.c:285 +#, c-format +msgid " [%s %s, %s]" +msgstr " [%s %s, %s]" + +#: alsamixer/mixer_display.c:294 alsamixer/mixer_display.c:300 +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:312 +msgid "Off" +msgstr "Itzalita" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:312 +msgid "On" +msgstr "Piztuta" + +#: alsamixer/mixer_display.c:363 +msgid "The sound device was unplugged." +msgstr "Soinu-txartela deskonektatu egin da." + +#: alsamixer/mixer_display.c:364 +msgid "Press F6 to select another sound card." +msgstr "Sakatu F6 beste soinu-txartel bat hautatzeko." + +#: alsamixer/mixer_display.c:379 +msgid "This sound device does not have any playback controls." +msgstr "Soinu-txartel honek ez du erreprodukzio kontrolik." + +#: alsamixer/mixer_display.c:381 +msgid "This sound device does not have any capture controls." +msgstr "Soinu-txartel honek ez du kapturatze kontrolik." + +#: alsamixer/mixer_display.c:383 +msgid "This sound device does not have any controls." +msgstr "Soinu-txartel honek ez du kontrolik." + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:516 alsamixer/mixer_display.c:521 +msgid "O" +msgstr "O" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:518 alsamixer/mixer_display.c:522 +msgid "M" +msgstr "M" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:536 +msgid "L" +msgstr "L" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:540 +msgid "R" +msgstr "R" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:542 +msgid "CAPTURE" +msgstr "KAPTURATZEA" + +#: alsamixer/mixer_display.c:592 +msgid "Front" +msgstr "Aurrealdekoa" + +#: alsamixer/mixer_display.c:595 +msgid "Rear" +msgstr "Atzealdekoa" + +#: alsamixer/mixer_display.c:598 speaker-test/speaker-test.c:125 +msgid "Center" +msgstr "Erdialdekoa" + +#: alsamixer/mixer_display.c:601 +msgid "Woofer" +msgstr "Wooferra" + +#: alsamixer/mixer_display.c:604 +msgid "Side" +msgstr "Aldekoa" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "ezin da ireki nahasgailua" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "ezin dira kargatu nahasgailu-kontrolak" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "Ezin da ireki '%s' nahasgailua." + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc Irten" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H Laguntza" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / Sistemaren informazioa" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 Erakutsi erreprodukzio kontrolak" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 Erakutsi kaptura kontrolak" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 Erakutsi kontrol guztiak" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab Aldatu ikuspegi modua (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F6 S Hautatu soinu-txartela" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L Pantaila birmarraztu" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "↠Mugitu ezkerreko kontrolera" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "→ Mugitu eskuineko kontrolera" + +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "↑ ↓ Bolumena aldatu" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - Bolumena aldatu" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "Orria↑/↓ Aldatu bolumena urrats handinaka" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "Amaiera Ezarri bolumena %0n" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 Ezarri bolumena %0-%90 bitartean" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E Handitu ezker/bi/eskuin bolumenak" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Z X C Txikitu ezker/bi/eskuin bolumenak" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B Orekatu ezker eta eskuin bolumenak" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M Mututu/Desmututu" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr "< > Mututu/Desmututu ezker/eskuin" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "Zuriunea Gaitu/Desgaitu kapturatzea" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "; ' Gaitu/Desgaitu ezker/eskuin kapturatzea" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "Egileak:" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr " Tim Janik" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr " Jaroslav Kysela " + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr " Clemens Ladisch " + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "Laguntza" + +#: alsamixer/proc_files.c:103 +msgid "Select File" +msgstr "Hautatu fitxategia" + +#: alsamixer/textbox.c:52 alsamixer/textbox.c:66 +msgid "Error" +msgstr "Errorea" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "Ezin da ireki \"%s\" fitxategia." + +#: aplay/aplay.c:178 +msgid "raw data" +msgstr "datu gordinak" + +#: aplay/aplay.c:179 +msgid "VOC" +msgstr "VOC" + +#: aplay/aplay.c:181 +msgid "WAVE" +msgstr "WAVE" + +#: aplay/aplay.c:182 +msgid "Sparc Audio" +msgstr "Sparc Audio" + +#: aplay/aplay.c:203 +#, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help laguntza\n" +" --version erakutsi oraingo bertsioa\n" +"-l, --list-devices zerrendatu soinu-txartel eta audio-digital gailu " +"guztiak\n" +"-L, --list-pcms zerrendatu gailuen izenak\n" +"-D, --device=IZENA hautatu PCM izenaren arabera\n" +"-q, --quiet modu isila\n" +"-t, --file-type MOTA fitxategi mota (voc, wav, raw edo au)\n" +"-c, --channels=# kanalak\n" +"-f, --format=FORMATUA laginaren formatua (ez dira bereizten maiuskulak eta " +"minuskulak)\n" +"-r, --rate=# lagin-tasa\n" +"-d, --duration=# gelditu # segundu ostean\n" +"-s, --samples=# gelditu kanaleko # lagin ostean\n" +"-M, --mmap mmap stream-a\n" +"-N, --nonblock nonblocking modua\n" +"-F, --period-time=# geldialdien arteko distantzia # mikrosegundokoa da\n" +"-B, --buffer-time=# bufferraren iraupena # mikrosegunfokoa da\n" +" --period-size=# geldialdien arteko distantzia # laginekoa da\n" +" --buffer-size=# bufferraren iraupena # laginekoa da\n" +"-A, --avail-min=# esnatzeko gutxieneko espazio eskuragarria # " +"mikrosegundo da\n" +"-R, --start-delay=# PCM automatikoki hasteko atzerapena # mikrosegundo " +"da \n" +" (buffer-tamainaren erlatiboa baldin eta <= 0)\n" +"-T, --stop-delay=# PCM automatikoki gelditzeko atzerapena # " +"mikrosegundo da xrun-etik\n" +"-v, --verbose erakutsi PCM egitura eta ezarpena (pilagarria)\n" +"-V, --vumeter=MOTA gaitu VU neurgailua (MOTA: mono edo stereo)\n" +"-I, --separate-channels fitxategi bat kanaleko\n" +"-i, --interactive baimendu ekintza interaktiboa stdin-etik\n" +"-m, --chmap=ch1,ch2,.. Eman kanal-mapa gainidatzi edo jarraitzeko\n" +" --disable-resample desgaitu tasa birlagintze automatikoa\n" +" --disable-channels desgaitu kanal bihurketa automatikoa\n" +" --disable-format desgaitu formatu bihurketa automatikoa\n" +" --disable-softvol desgaitu software volume control (softvol)\n" +" --test-position probatu eraztun-bufferraren kokalekua\n" +" --test-coef=# probatu eraztun-bufferraren kokalekuarentzako " +"koefizientea (lehenetsia 8)\n" +" baliozkotzeko adierazpena hau da: koef * " +"(buffer_tamaina / 2)\n" +" --test-nowait ez itxaron eraztun-bufferrari - PUZ osoa erabiliko " +"du\n" +" --max-file-time=# beste irteera-fitxategi bat abiarazi, fitxategi " +"zaharrak\n" +" segundo askotarako grabatu duenean\n" +" --process-id-file erakutsi hemen prozesuaren IDa\n" +" --use-strftime aplikatu strftime instalazioa irteera-fitxategiaren " +"izenari \n" +" --dump-hw-params ezkutatu gailuaren hw parametroak\n" +" --fatal-errors jo errore guztiak larritzat\n" + +#: aplay/aplay.c:248 speaker-test/speaker-test.c:1023 +#, c-format +msgid "Recognized sample formats are:" +msgstr "Hauek dira onartutako lagin-formatuak:" + +#: aplay/aplay.c:254 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"Baliteke horietako batzuk erabilgarri ez egotea hautatutako hardwarean\n" + +#: aplay/aplay.c:255 +#, c-format +msgid "The available format shortcuts are:\n" +msgstr "Hauek dira formatu laster-tekla erabilgarriak:\n" + +#: aplay/aplay.c:256 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16 bit bukaera txikia, 44100, estereo)\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16 bit bukaera handia, 44100, estereo)\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16 bit bukaera txikia, 48000, estereo)\n" + +#: aplay/aplay.c:272 +msgid "no soundcards found..." +msgstr "ez da soinu-txartelik aurkitu..." + +#: aplay/aplay.c:275 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** %s hardware gailuen zerrenda ****\n" + +#: aplay/aplay.c:304 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "%i txartela: %s [%s], %i gailua: %s [%s]\n" + +#: aplay/aplay.c:310 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " Azpigailuak: %i/%i\n" + +#: aplay/aplay.c:317 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " #%i azpigailua: %s\n" + +#: aplay/aplay.c:398 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "%s seinaleak abortatuta...\n" + +#: aplay/aplay.c:553 +msgid "command should be named either arecord or aplay" +msgstr "komandoa arecord edo aplay gisa izendatu behar da" + +#: aplay/aplay.c:597 +#, c-format +msgid "unrecognized file format %s" +msgstr "%s fitxategi-formatu ezezaguna" + +#: aplay/aplay.c:604 +#, c-format +msgid "invalid channels argument '%s'" +msgstr "kanalen '%s' argumentu baliogabea" + +#: aplay/aplay.c:608 +#, c-format +msgid "value %i for channels is invalid" +msgstr "kanalentzako %i balio baliogabea" + +#: aplay/aplay.c:627 +#, c-format +msgid "wrong extended format '%s'" +msgstr "'%s' formatu hedatua ez da zuzena" + +#: aplay/aplay.c:635 +#, c-format +msgid "invalid rate argument '%s'" +msgstr "'%s' tasa argumentu baliogabea" + +#: aplay/aplay.c:642 +#, c-format +msgid "bad speed value %i" +msgstr "%i abiadura balio okerra" + +#: aplay/aplay.c:648 +msgid "duration and samples arguments cannot be used together" +msgstr "iraupen eta lagin argumentuak ezin dira elkarrekin erabili" + +#: aplay/aplay.c:653 +#, c-format +msgid "invalid duration argument '%s'" +msgstr "'%s' iraupen argumentu baliogabea" + +#: aplay/aplay.c:660 +msgid "samples and duration arguments cannot be used together" +msgstr "lagin eta iraupen argumentuak ezin dira elkarrekin erabili" + +#: aplay/aplay.c:665 +#, c-format +msgid "invalid samples argument '%s'" +msgstr "'%s' lagin argumentu baliogabea" + +#: aplay/aplay.c:677 +#, c-format +msgid "invalid period time argument '%s'" +msgstr "'%s' periodo-denbora argumentu baliogabea" + +#: aplay/aplay.c:684 +#, c-format +msgid "invalid buffer time argument '%s'" +msgstr "'%s' buffer-denbora argumentu baliogabea" + +#: aplay/aplay.c:691 +#, c-format +msgid "invalid period size argument '%s'" +msgstr "'%s' periodo-tamaina argumentu baliogabea" + +#: aplay/aplay.c:698 +#, c-format +msgid "invalid buffer size argument '%s'" +msgstr "'%s' buffer-tamaina argumentu baliogabea" + +#: aplay/aplay.c:705 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "'%s' gutxieneko espazio erabilgarri argumentu baliogabea" + +#: aplay/aplay.c:712 +#, c-format +msgid "invalid start delay argument '%s'" +msgstr "'%s' haste-atzerapen argumentu baliogabea" + +#: aplay/aplay.c:719 +#, c-format +msgid "invalid stop delay argument '%s'" +msgstr "'%s' gelditze-atzerapen argumentu baliogabea" + +#: aplay/aplay.c:774 +#, c-format +msgid "invalid test coef argument '%s'" +msgstr "'%s' koef. proba argumentu baliogabea" + +#: aplay/aplay.c:786 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "'%s' gehienezko fitxategi-denbora argumentu baliogabea" + +#: aplay/aplay.c:806 speaker-test/speaker-test.c:666 +#, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "Ezin da kanal-maparen katea analizatu: %s\n" + +#: aplay/aplay.c:812 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "Probatu `%s --help' xehetasun gehiagorako.\n" + +#: aplay/aplay.c:828 +#, c-format +msgid "audio open error: %s" +msgstr "audio irekitze errorea: %s" + +#: aplay/aplay.c:833 +#, c-format +msgid "info error: %s" +msgstr "informazio errorea: %s" + +#: aplay/aplay.c:840 +#, c-format +msgid "nonblock setting error: %s" +msgstr "nonblock-en konfigurazio-ezarpen errorea: %s" + +#: aplay/aplay.c:850 aplay/aplay.c:974 aplay/aplay.c:1264 aplay/aplay.c:1458 +#: aplay/aplay.c:1979 aplay/aplay.c:2009 +msgid "not enough memory" +msgstr "ez dago behar adina memoria" + +#: aplay/aplay.c:874 +#, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "Ezin da sortu %s prozesuaren ID fitxategia: %s" + +#: aplay/aplay.c:964 +#, c-format +msgid "read error (called from line %i)" +msgstr "irakurtzean errorea (%i lerrotik deitua)" + +#: aplay/aplay.c:1031 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "'fmt' chunk luzera ezezaguna (%u irakurri, %u izan behar du gutxienez)" + +#: aplay/aplay.c:1042 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"'fmt' chunk hedagarri luzera ezezaguna (%u irakurri, %u izan behar du " +"gutxienez)" + +#: aplay/aplay.c:1047 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "formatua ez da zuzena 'fmt' chunk hedagarrian" + +#: aplay/aplay.c:1054 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "" +"ezin da erreproduzitu PCM edo FLOAT kodetua ez den 0x%04x WAVE-fitxategiaren " +"formatua" + +#: aplay/aplay.c:1059 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "ezin dira erreproduzitu %d pista dituzten WAVE-fitxategiak" + +#: aplay/aplay.c:1067 aplay/aplay.c:1193 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "Abisua: formatua U8-ra aldatu da\n" + +#: aplay/aplay.c:1077 aplay/aplay.c:1090 aplay/aplay.c:1101 +#, c-format +msgid "Warning: format is changed to %s\n" +msgstr "Abisua: formatua %s-(e)ra aldatu da\n" + +#: aplay/aplay.c:1106 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr "" +" ezin dira erreproduzitu %d byteko zabaleran %d lagin-bita duten WAVE-" +"fitxategiak (%d kanal)" + +#: aplay/aplay.c:1129 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr "" +" ezin dira erreproduzitu %d lagin-biteko zabalera duten WAVE-fitxategiak" + +#: aplay/aplay.c:1187 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "Abisua: formatua MU_LAW-era aldatu da\n" + +#: aplay/aplay.c:1199 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "Abisua: formatua S16_BE-ra aldatu da\n" + +#: aplay/aplay.c:1212 aplay/aplay.c:2294 aplay/aplay.c:2301 aplay/aplay.c:2831 +msgid "read error" +msgstr "irakurtze errorea" + +#: aplay/aplay.c:1242 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "Kanal-zenbakiak ez datoz bat hw parametro eta kanal-maparen artean" + +#: aplay/aplay.c:1251 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "Abisua: ezin dat kanal-mapa eskuratu\n" + +#: aplay/aplay.c:1284 +#, c-format +msgid "Channel %d doesn't match with hw_parmas" +msgstr "%d kanala ez dator bat hw parametroekin" + +#: aplay/aplay.c:1310 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "" +"PCM honetarako konfigurazio hautsia: ez dago konfigurazio erabilgarririk" + +#: aplay/aplay.c:1314 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "\"%s\" gailuaren HW parametroak:\n" + +#: aplay/aplay.c:1334 +msgid "Access type not available" +msgstr "Sarbide-mota ez dago erabilgarri" + +#: aplay/aplay.c:1339 +msgid "Sample format non available" +msgstr "Lagin formatua ez dago erabilgarri" + +#: aplay/aplay.c:1345 +msgid "Channels count non available" +msgstr "Kanal zenbaketa ez dago erabilgarri" + +#: aplay/aplay.c:1360 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "" +"Abisua: lagin-tasa ez da zehatza (eskatutakoa = %iHz, eskuratutakoa = %iHz)\n" + +#: aplay/aplay.c:1366 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " probatu %s konektatze-plugina\n" + +#: aplay/aplay.c:1403 +msgid "Unable to install hw params:" +msgstr "Ezin dira hw parametroak instalatu:" + +#: aplay/aplay.c:1410 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "Ezin da bufferraren tamaina bereko periodoa erabili (%lu == %lu)" + +#: aplay/aplay.c:1441 +msgid "unable to install sw params:" +msgstr "ezin dira sw parametroak instalatu:" + +#: aplay/aplay.c:1476 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "snd_pcm_mmap_begin arazoa: %s" + +#: aplay/aplay.c:1505 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "stdin O_NONBLOCK marka ezartzeak huts egin du\n" + +#: aplay/aplay.c:1529 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "\rPAUSE komandoa ezikusi da (ez du hw sostengurik)\n" + +#: aplay/aplay.c:1537 +#, c-format +msgid "pause push error: %s" +msgstr "pause push errorea: %s" + +#: aplay/aplay.c:1548 +#, c-format +msgid "pause release error: %s" +msgstr "pause release errorea: %s" + +#: aplay/aplay.c:1564 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" +"\r=== PAUSE === " + +#: aplay/aplay.c:1606 +#, c-format +msgid "status error: %s" +msgstr "egoera errorea: %s" + +#: aplay/aplay.c:1611 +#, c-format +msgid "fatal %s: %s" +msgstr "%s larria: %s" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1626 aplay/aplay.c:1634 +msgid "underrun" +msgstr "underrun (azpiratzea)" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1634 +msgid "overrun" +msgstr "overrun (gainditzea)" + +#: aplay/aplay.c:1622 aplay/aplay.c:1633 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (behintzat %.3f ms luze)\n" + +#: aplay/aplay.c:1638 +#, c-format +msgid "Status:\n" +msgstr "Egoera:\n" + +#: aplay/aplay.c:1642 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "xrun: prestakuntza errorea: %s" + +#: aplay/aplay.c:1648 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "Egoera(HUSTEN):\n" + +#: aplay/aplay.c:1652 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "kaptura-fluxu formatua aldatu? berreskuratzea saiatzen...\n" + +#: aplay/aplay.c:1654 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "xrun(HUSTEN): prestakuntza errorea: %s" + +#: aplay/aplay.c:1661 +#, c-format +msgid "Status(R/W):\n" +msgstr "Egoera(Irak/Idat):\n" + +#: aplay/aplay.c:1664 +#, c-format +msgid "read/write error, state = %s" +msgstr "irakurtze/idazte errorea, egoera = %s" + +#: aplay/aplay.c:1674 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "Etenda. Berrekiten saiatzen. " + +#: aplay/aplay.c:1679 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "Huts egin du. Fluxua berrabiarazten. " + +#: aplay/aplay.c:1681 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "eten: prestakuntza errorea: %s" + +#: aplay/aplay.c:1686 +#, c-format +msgid "Done.\n" +msgstr "Egina.\n" + +#: aplay/aplay.c:1708 +#, c-format +msgid " !clip " +msgstr " !clip " + +#: aplay/aplay.c:1855 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "Euskarririk gabeko %d bit-tamaina.\n" + +#: aplay/aplay.c:1889 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "Gehiengo gailurra (%li lagin): 0x%08x " + +#: aplay/aplay.c:1923 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "" +"Buffer-kokaleku susmagarria (%li guztira) eskurag = %li, atzerapena = %li, " +"bufferra = %li\n" + +#: aplay/aplay.c:2051 +#, c-format +msgid "write error: %s" +msgstr "idazte errorea: %s" + +#: aplay/aplay.c:2099 +#, c-format +msgid "writev error: %s" +msgstr "writev errorea: %s" + +#: aplay/aplay.c:2143 +#, c-format +msgid "read error: %s" +msgstr "irakurtze errorea: %s" + +#: aplay/aplay.c:2187 +#, c-format +msgid "readv error: %s" +msgstr "readv errorea: %s" + +#: aplay/aplay.c:2235 +msgid "can't allocate buffer for silence" +msgstr "ezin da esleitu bufferra isiltasunerako" + +#: aplay/aplay.c:2244 aplay/aplay.c:2472 aplay/aplay.c:2477 aplay/aplay.c:2527 +#: aplay/aplay.c:2536 aplay/aplay.c:2543 aplay/aplay.c:2553 aplay/aplay.c:2559 +#: aplay/aplay.c:2631 aplay/aplay.c:2661 aplay/aplay.c:2675 +msgid "write error" +msgstr "idazte errorea" + +#: aplay/aplay.c:2257 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "voc_pcm_flush - isiltasun errorea" + +#: aplay/aplay.c:2260 +msgid "voc_pcm_flush error" +msgstr "voc_pcm_flush errorea" + +#: aplay/aplay.c:2285 +msgid "malloc error" +msgstr "malloc errorea" + +#: aplay/aplay.c:2289 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "'%s' Creative Labs Channel fitxategia erreproduzitzen...\n" + +#: aplay/aplay.c:2357 aplay/aplay.c:2452 +msgid "can't play packed .voc files" +msgstr "ezin dira erreproduzitu paketatutako .voc fitxategiak" + +#: aplay/aplay.c:2412 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "ezin dira begiztak erreproduzitu; %s ez da bilagarria\n" + +#: aplay/aplay.c:2461 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "%d bloke-mota ezezaguna. Amaitu." + +#: aplay/aplay.c:2595 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "Wave-ek ez du %s formatua onartzen..." + +#: aplay/aplay.c:2655 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "Sparc Audio-k ez du %s formatua onartzen..." + +#: aplay/aplay.c:2736 +msgid "Playing" +msgstr "Erreproduzitzen" + +#: aplay/aplay.c:2736 +msgid "Recording" +msgstr "Grabatzen" + +#: aplay/aplay.c:2740 +#, c-format +msgid "Rate %d Hz, " +msgstr "Tasa %d Hz, " + +#: aplay/aplay.c:2742 +#, c-format +msgid "Mono" +msgstr "Mono" + +#: aplay/aplay.c:2744 +#, c-format +msgid "Stereo" +msgstr "Estereo" + +#: aplay/aplay.c:2746 +#, c-format +msgid "Channels %i" +msgstr "Kanalak %i" + +#: aplay/aplay.c:3344 aplay/aplay.c:3397 +#, c-format +msgid "You need to specify %d files" +msgstr "%d fitxategiak zehaztu behar dituzu" + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - ALSA konexio sekuentziadorearen kudeatzailea\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "Copyright (C) 1999-2000 Takashi Iwai\n" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "Erabilera:\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * Bi ataken arteko konexio/deskonexioak\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [-aukerak] bidaltzailea hartzailea\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " bidaltzailea, hartzailea = bezeroa:ataka parea\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect deskonektatu\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive konexio esklusiboa\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # sartu denbora-errealeko-zigilua ilaran\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # sartu denbora-marka-zigilua ilaran\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * Zerrendatu konektatutako atakak (harpidetza-ekintzarik ez)\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr " aconnect -i|-o [-aukerak]\n" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr "" +" -i,--input zerrendatu sarrera-atakak (irakurgarriak)\n" +"\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr " -o,--output zerrendatu irteera-atakak (idazgarriak)\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr "" +" -l,--list zerrendatu ataka bakoitzak orain dituen loturak\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr "" +" * Ezabatu esportatutako konexio guztiak\n" +"\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr " -x, --removeall\n" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "Hona konektatzen" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "Hemendik konektatuta" + +#: seq/aconnect/aconnect.c:171 +#, c-format +msgid "client %d: '%s' [type=%s" +msgstr "%d bezeroa: '%s' [mota=%s" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "erabiltzailea" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "kernela" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "ezin da ireki sekuentziadorea\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "ezin da lortu bezeroaren IDa\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "ezin da zehaztu bezeroaren informazioa\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "%s bidaltzaile-helbide baliogabea\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:290 +#, c-format +msgid "invalid destination address %s\n" +msgstr "%s hartzaile-helbide baliogabea\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "Ez da harpidetzarik aurkitu\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "Deskonektatzeak huts egin du (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "Konexioa jada harpidetua dago\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "Konexioak huts egin du (%s)\n" + +#: seq/aseqnet/aseqnet.c:164 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - ALSA sekuentziadorearen sarerako bezeroa/zerbitzaria\n" + +#: seq/aseqnet/aseqnet.c:165 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr " Copyright (C) 1999 Takashi Iwai\n" + +#: seq/aseqnet/aseqnet.c:166 +#, c-format +msgid "usage:\n" +msgstr "erabilera:\n" + +#: seq/aseqnet/aseqnet.c:167 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " zerbitzari modua: aseqnet [-aukerak]\n" + +#: seq/aseqnet/aseqnet.c:168 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " bezero modua: aseqnet [-aukerak] zerbitzari_ostalaria\n" + +#: seq/aseqnet/aseqnet.c:169 +#, c-format +msgid "options:\n" +msgstr "aukerak:\n" + +#: seq/aseqnet/aseqnet.c:170 +#, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : zehaztu TCP ataka (zerbitzuaren digitua edo izena)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr "" +" -s,--source helbidea : irakurri emandako helbidetik (bezeroa:ataka)\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest helbidea : idatzi emandako helbidean (bezeroa:ataka)\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v, --verbose : erakutsi mezu xehatuak\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i, --info : erakutsi jasotako zenbait gertaera\n" + +#: seq/aseqnet/aseqnet.c:188 +#, c-format +msgid "can't malloc\n" +msgstr "ezin da malloc exekutatu\n" + +#: seq/aseqnet/aseqnet.c:213 +#, c-format +msgid "closing files..\n" +msgstr "fitxategiak ixten...\n" + +#: seq/aseqnet/aseqnet.c:272 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "sekuentziadorea ireki da: %d:%d\n" + +#: seq/aseqnet/aseqnet.c:279 +#, c-format +msgid "invalid source address %s\n" +msgstr "%s iturburu-helbide baliogabea\n" + +#: seq/aseqnet/aseqnet.c:309 +#, c-format +msgid "service '%s' is not found in /etc/services\n" +msgstr "ez da aurkitu '%s' zerbitzua /etc/services direktorioan\n" + +#: seq/aseqnet/aseqnet.c:377 +#, c-format +msgid "too many connections!\n" +msgstr "konexio gehiegi!\n" + +#: seq/aseqnet/aseqnet.c:388 +#, c-format +msgid "accepted[%d]\n" +msgstr "onartua[%d]\n" + +#: seq/aseqnet/aseqnet.c:411 +#, c-format +msgid "can't get address %s\n" +msgstr "ezin da %s helbidea lortu\n" + +#: seq/aseqnet/aseqnet.c:422 +#, c-format +msgid "ok.. connected\n" +msgstr "ados... konektatuta\n" + +#: seq/aseqnet/aseqnet.c:518 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "%2d kanala: kontrol-gertaera : %5d\n" + +#: seq/aseqnet/aseqnet.c:522 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "%2d kanala: Tonu aldatzailea : %5d\n" + +#: seq/aseqnet/aseqnet.c:526 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "%2d kanala: 'Oharra gaituta' gertaera : %5d\n" + +#: seq/aseqnet/aseqnet.c:530 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "%2d kanala: 'Oharra desgaituta' gertaera : %5d\n" + +#: seq/aseqnet/aseqnet.c:585 +#, c-format +msgid "disconnected\n" +msgstr "deskonektatuta\n" + +#: speaker-test/speaker-test.c:121 +msgid "Front Left" +msgstr "Aurrealde ezkerra" + +#: speaker-test/speaker-test.c:122 +msgid "Front Right" +msgstr "Aurrealde eskuina" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Left" +msgstr "Atzealde ezkerra" + +#: speaker-test/speaker-test.c:124 +msgid "Rear Right" +msgstr "Atzealde eskuina" + +#: speaker-test/speaker-test.c:126 +msgid "LFE" +msgstr "LFE" + +#: speaker-test/speaker-test.c:127 +msgid "Side Left" +msgstr "Ezker aldea" + +#: speaker-test/speaker-test.c:128 +msgid "Side Right" +msgstr "Eskuin aldea" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 9" +msgstr "9 kanala" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 10" +msgstr "10 kanala" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 11" +msgstr "11 kanala" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 12" +msgstr "12 kanala" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 13" +msgstr "13 kanala" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 14" +msgstr "14 kanala" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 15" +msgstr "15 kanala" + +#: speaker-test/speaker-test.c:136 +msgid "Channel 16" +msgstr "16 kanala" + +#: speaker-test/speaker-test.c:519 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "" +"Erreprodukziorako konfigurazio hautsia: ez dago konfigurazio erabilgarririk: " +"%s\n" + +#: speaker-test/speaker-test.c:526 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "Sarbide mota ez erabilgarri erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:533 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "Lagin formatua ez erabilgarri erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:540 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "Kanal zenbaketa (%i) ez erabilgarri erreprodukzioetarako: %s \n" + +#: speaker-test/speaker-test.c:548 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "%iHz lagin-tasa ez erabilgarri erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:553 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "" +"Lagin-tasa ez dator bat (eskatutakoa %iHz, eskuratutakoa %iHz, errorea %d)\n" + +#: speaker-test/speaker-test.c:557 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "Lagin-tasa %iHz-tan ezarria (eskatutakoa %iHz)\n" + +#: speaker-test/speaker-test.c:563 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "Buffer tamainaren barrutia %lu(e)tik %lu(e)ra\n" + +#: speaker-test/speaker-test.c:564 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "Periodo tamainaren barrutia %lu(e)tik %lu(e)ra\n" + +#: speaker-test/speaker-test.c:566 +#, c-format +msgid "Requested period time %u us\n" +msgstr "Eskatutako periodo denbora %u µs\n" + +#: speaker-test/speaker-test.c:569 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "Ezin da ezarri periodo denbora %u µs gisa erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:575 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "Eskatutako buffer denbora %u µs\n" + +#: speaker-test/speaker-test.c:578 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "Ezin da ezarri buffer denbora %u µs gisa erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:587 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "Buffer tamaina handiena erabiltzen: %lu\n" + +#: speaker-test/speaker-test.c:590 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "Ezin da ezarri buffer tamaina %lu gisa erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:596 +#, c-format +msgid "Periods = %u\n" +msgstr "Periodoak = %u\n" + +#: speaker-test/speaker-test.c:599 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "Ezin dira ezarri nperiods-ak %u gisa erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:608 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "Ezin dira ezarri hw parametroak erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:614 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "period_size ezarri da = %lu\n" + +#: speaker-test/speaker-test.c:615 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "buffer_size ezarri da = %lu\n" + +#: speaker-test/speaker-test.c:617 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "buffer txikiegia, ezin izan da erabili\n" + +#: speaker-test/speaker-test.c:630 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "Ezin dira zehaztu oraingo sw parametroak erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:637 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "Ezin da ezarri 'hasi atalasea' modua erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:644 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "Ezin da ezarri 'avail min' erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:651 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "Ezin dira ezarri sw parametroak erreprodukziorako: %s\n" + +#: speaker-test/speaker-test.c:671 +#, c-format +msgid "Unable to set channel map: %s\n" +msgstr "Ezin da ezarri kanal-mapa: %s\n" + +#: speaker-test/speaker-test.c:696 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "" +"Ezin da berreskuratu azpiratzetik (underrun), prestakuntzak huts egin du: " +"%s\n" + +#: speaker-test/speaker-test.c:707 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "Ezin da berreskuratu esekitzetik, prestakuntzak huts egin du: %s\n" + +#: speaker-test/speaker-test.c:771 speaker-test/speaker-test.c:1272 +#, c-format +msgid "No enough memory\n" +msgstr "Ez dago behar adina memoria\n" + +#: speaker-test/speaker-test.c:776 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "Ezin da ireki %s WAV fitxategia\n" + +#: speaker-test/speaker-test.c:780 speaker-test/speaker-test.c:809 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "%s WAV fitxategi baliogabea\n" + +#: speaker-test/speaker-test.c:785 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "Ez da WAV fitxategia: %s\n" + +#: speaker-test/speaker-test.c:789 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "Euskarririk gabeko %d WAV formatua %s(r)entzat\n" + +#: speaker-test/speaker-test.c:794 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s ez da mono bidalketa (%d kanal)\n" + +#: speaker-test/speaker-test.c:799 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "Lagin-tasa ez dator bat (%d) %s(r)ekin\n" + +#: speaker-test/speaker-test.c:804 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "Euskarririk gabeko %d lagin formatu bitak %s(r)entzat\n" + +#: speaker-test/speaker-test.c:865 +#, c-format +msgid "Undefined channel %d\n" +msgstr "%d kanal zehaztugabea\n" + +#: speaker-test/speaker-test.c:916 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "Idazte errorea: %d,%s\n" + +#: speaker-test/speaker-test.c:918 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery ekintzak huts egin du: %d,%s\n" + +#: speaker-test/speaker-test.c:1004 +#, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"Erabilera: speaker-test [AUKERA]... \n" +"-h,--help\tlaguntza\n" +"-D,--device\terreprodukzio gailua\n" +"-r,--rate\tbidalketaren lagin-tasa Hz-tan\n" +"-c,--channels\tbidalt¡keta burutzen ari diren kanalen zenbaketa\n" +"-f,--frequency\tuhin sinusoidalaren frekuentzia Hz-tan\n" +"-F,--format\tlagin formatua\n" +"-b,--buffer\teraztun-bufferraren tamaina µs-tan\n" +"-p,--period\tperiodo taimaina µs-tan\n" +"-P,--nperiods\tperiodo kopurua\n" +"-t,--test\tpink=erabili soinu arrosa, sine=erabili uhin sinusoidala, wav=WAV " +"fitxategia\n" +"-l,--nloops\tzehaztu proba egiteko begizta kopurua, 0 = mugagabe\n" +"-s,--speaker\tbozgorailu bakarreko proba. Balioak: 1=Ezkerra, 2=Eskuina, " +"etb.\n" +"-w,--wavfile\tErabili emandako WAV fitxategia probarako soinutzat\n" +"-W,--wavdir\tZehaztu WAV fitxategiak dituen direktorioa\n" +"-m,--chmap\tZehaztu gainidatziko den kanal-mapa\n" +"-X,--force-frequency\tbehartu 30-8.000Hz barruti kanpoko frekuentziak\n" +"-S,--scale\tSortutako proba-tonuen eskala portzentaian (lehenetsia=80)\n" +"\n" + +#: speaker-test/speaker-test.c:1136 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "%d periodo kopuru baliogabea\n" + +#: speaker-test/speaker-test.c:1152 speaker-test/speaker-test.c:1156 +#, c-format +msgid "Invalid test type %s\n" +msgstr "%s proba mota baliogabea\n" + +#: speaker-test/speaker-test.c:1189 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "'%c' aukera ezezaguna\n" + +#: speaker-test/speaker-test.c:1203 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "-s aukerarako parametro baliogabea.\n" + +#: speaker-test/speaker-test.c:1218 +#, c-format +msgid "Playback device is %s\n" +msgstr "Erreprodukzio gailua %s da\n" + +#: speaker-test/speaker-test.c:1219 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "Erreprodukzio parametroak %iHz, %s, %i kanal dira\n" + +#: speaker-test/speaker-test.c:1222 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "Soinu arroseko 16 zortzidun erabiltzen\n" + +#: speaker-test/speaker-test.c:1225 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "Uhin sinusoidalaren tasa %.4fHz da\n" + +#: speaker-test/speaker-test.c:1228 +#, c-format +msgid "WAV file(s)\n" +msgstr "" +"WAV fitxategia(k)\n" +"\n" + +#: speaker-test/speaker-test.c:1238 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "Erreprodukzio irekitze errorea: %d,%s\n" + +#: speaker-test/speaker-test.c:1244 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "hwparams-eko hobespenek huts egin dute: %s\n" + +#: speaker-test/speaker-test.c:1248 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "swparams-eko hobespenek huts egin dute: %s\n" + +#: speaker-test/speaker-test.c:1295 speaker-test/speaker-test.c:1317 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "Bidalketak huts egin du: %s\n" + +#: speaker-test/speaker-test.c:1303 +#, c-format +msgid "Time per period = %lf\n" +msgstr "Denbora periodoko = %lf\n" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 0000000..cb4fc3f --- /dev/null +++ b/po/fr.po @@ -0,0 +1,1713 @@ +# French translation for alsa-utils +# Copyright (C) 2011 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Christoph J. Thompson , 2011. +# Olivier Humbert , 2020 +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.23\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-10 09:07+0100\n" +"PO-Revision-Date: 2020-07-08 16:05+0100\n" +"Last-Translator: Olivier Humbert \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: alsamixer/card_select.c:126 alsamixer/device_name.c:126 +msgid "Sound Card" +msgstr "Carte Son" + +#: alsamixer/card_select.c:181 +msgid "(default)" +msgstr "(par défaut)" + +#: alsamixer/card_select.c:191 +msgid "cannot enumerate sound cards" +msgstr "les cartes son n'ont pas pu être énumérées" + +#: alsamixer/card_select.c:215 +msgid "enter device name..." +msgstr "entrez le nom du périphérique..." + +#: alsamixer/cli.c:40 +msgid "Usage: alsamixer [options]" +msgstr "Utilisation : alsamixer [options]" + +#: alsamixer/cli.c:41 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"Options utiles:\n" +" -h, --help cette aide\n" +" -c, --card=NUMÉRO numéro ou identifiant de la carte son\n" +" -D, --device=NOM nom du périphérique de mixage\n" +" -V, --view=MODE mode de visualisation par défaut : lecture/capture/tous" + +#: alsamixer/cli.c:46 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"Options de débogage:\n" +" -g, --no-color basculer l'utilisation de la couleur\n" +" -a, --abstraction=NOM niveau d'abstraction du mixeur : aucun/basique" + +#: alsamixer/cli.c:77 +#, c-format +msgid "invalid card index: %s\n" +msgstr "index de carte invalide : %s\n" + +#: alsamixer/cli.c:103 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "niveau d'abstraction inconnu : %s\n" + +#: alsamixer/cli.c:108 +#, c-format +msgid "unknown option: %c\n" +msgstr "option inconnue : %c\n" + +#: alsamixer/cli.c:110 +msgid "try `alsamixer --help' for more information\n" +msgstr "essayez `alsamixer --help' pour plus d'informations\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "Nom du périphérique :" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "%s: %s\n" + +#: alsamixer/mixer_display.c:98 +msgid "Card:" +msgstr "Carte :" + +#: alsamixer/mixer_display.c:99 +msgid "Chip:" +msgstr "Puce :" + +#: alsamixer/mixer_display.c:100 +msgid "View:" +msgstr "Vue :" + +#: alsamixer/mixer_display.c:101 +msgid "Item:" +msgstr "Élement :" + +#: alsamixer/mixer_display.c:104 +msgid "F1: Help" +msgstr "F1 : Aide" + +#: alsamixer/mixer_display.c:105 +msgid "F2: System information" +msgstr "F2 : Informations système" + +#: alsamixer/mixer_display.c:106 +msgid "F6: Select sound card" +msgstr "F6 : Choisir la carte son" + +#: alsamixer/mixer_display.c:107 +msgid "Esc: Exit" +msgstr "Esc : quitter" + +#: alsamixer/mixer_display.c:174 +msgid "(unplugged)" +msgstr "(non branché)" + +#: alsamixer/mixer_display.c:192 +msgid "Playback" +msgstr "Lecture" + +#: alsamixer/mixer_display.c:193 +msgid "Capture" +msgstr "Capture" + +#: alsamixer/mixer_display.c:194 +msgid "All" +msgstr "Tout" + +#: alsamixer/mixer_display.c:234 +msgid "mute" +msgstr "muet" + +#: alsamixer/mixer_display.c:275 alsamixer/mixer_display.c:285 +msgid "dB gain:" +msgstr "gain dB :" + +#: alsamixer/mixer_display.c:285 +#, c-format +msgid " [%s %s, %s]" +msgstr " [%s %s, %s]" + +#: alsamixer/mixer_display.c:294 alsamixer/mixer_display.c:300 +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:312 +msgid "Off" +msgstr "Fermé" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:312 +msgid "On" +msgstr "Ouvert" + +#: alsamixer/mixer_display.c:363 +msgid "The sound device was unplugged." +msgstr "Le périphérique son a été débranché" + +#: alsamixer/mixer_display.c:364 +msgid "Press F6 to select another sound card." +msgstr "Appuyez sur F6 pour sélectionner une autre carte son." + +#: alsamixer/mixer_display.c:379 +msgid "This sound device does not have any playback controls." +msgstr "Ce périphérique son n'a pas de commandes de lecture." + +#: alsamixer/mixer_display.c:381 +msgid "This sound device does not have any capture controls." +msgstr "Ce périphérique son n'a pas de commandes de capture." + +#: alsamixer/mixer_display.c:383 +msgid "This sound device does not have any controls." +msgstr "Ce périphérique son n'a pas de commandes." + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:516 alsamixer/mixer_display.c:521 +msgid "O" +msgstr "O" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:518 alsamixer/mixer_display.c:522 +msgid "M" +msgstr "M" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:536 +msgid "L" +msgstr "G" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:540 +msgid "R" +msgstr "D" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:542 +msgid "CAPTURE" +msgstr "CAPTURE" + +#: alsamixer/mixer_display.c:592 +msgid "Front" +msgstr "Avant" + +#: alsamixer/mixer_display.c:595 +msgid "Rear" +msgstr "Arrière" + +#: alsamixer/mixer_display.c:598 speaker-test/speaker-test.c:125 +msgid "Center" +msgstr "Centre" + +#: alsamixer/mixer_display.c:601 +msgid "Woofer" +msgstr "Basses" + +#: alsamixer/mixer_display.c:604 +msgid "Side" +msgstr "Côté" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "le mixeur ne peut pas être ouvert" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "les contrôles de mixage ne peuvent pas être chargés" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "Le périphérique de mixage '%s' ne peut pas être ouvert." + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc Quitter" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H Aide" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / Informations système" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 Afficher les contrôles de lecture" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 Afficher les contrôles de capture" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 Afficher tous les contrôles" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab Basculer le mode de visualisation (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F6 S Choisir la carte son" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L Rafraîchir l'écran" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "Gauche Aller au contrôle précédent" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "Droite Aller au contrôle suivant" + +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "Haut/Bas Modifier le volume" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - Modifier le volume" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "Page Préc./Suiv. Modifier le volume par grands intervalles" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "Fin Couper le volume" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 Ajuster le volume entre 0 et 90%" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E Augmenter les volumes de gauche/centre/droite" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Z X C Baisser les volumes de gauche/centre/droite" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B Égaliser les volumes de gauche et droite" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M Basculer le mode muet" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr "< > Basculer le mode muet à gauche et à droite" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "Espace Basculer la capture" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "; Basculer la capture à gauche et à droite" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "Auteurs :" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr "" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr "" + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr "" + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "Aide" + +#: alsamixer/proc_files.c:103 +msgid "Select File" +msgstr "Choisir un fichier" + +#: alsamixer/textbox.c:52 alsamixer/textbox.c:66 +msgid "Error" +msgstr "Erreur" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "Le fichier \"%s\" n'a pas pu être ouvert." + +#: aplay/aplay.c:178 +msgid "raw data" +msgstr "données brutes" + +#: aplay/aplay.c:179 +msgid "VOC" +msgstr "" + +#: aplay/aplay.c:181 +msgid "WAVE" +msgstr "" + +#: aplay/aplay.c:182 +msgid "Sparc Audio" +msgstr "" + +#: aplay/aplay.c:203 +#, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"Utilisation: %s [OPTION]... [FICHIER]...\n" +"\n" +"-h, --help aide\n" +" --version afficher la version du programme\n" +"-l, --list-devices lister tous périphériques audio-numériques et cartes son\n" +"-L, --list-pcms lister les noms de périphériques\n" +"-D, --device=NOM choisir le périphérique PCM par son nom\n" +"-Q, --quiet mode silencieux\n" +"-t, --file-type TYPE type de fichier (voc, wav, raw ou au)\n" +"-c, --channels=# canaux\n" +"-f, --format=FORMAT format d'échantillonage (insensible à la casse)\n" +"-r, --rate=# fréquence d'échantillonage\n" +"-d, --duration=# interrompre après # secondes\n" +"-s, --samples=# interrompre après # échantillons par canal\n" +"-M, --mmap flux mmap\n" +"-N, --nonblock mode non-bloquant\n" +"-F, --period-time=# la durée entre les interruptions est de\n" +"# microsecondes\n" +"-B, --buffer-time=# la durée du tampon est de # microsecondes\n" +" --period-size=# la durée entre interruptions est # trames\n" +" --buffer-size=# la durée du tampon est de # trames\n" +"-A, --avail-min=# l'espace minimum disponible pour le réveil est de\n" +"# microsecondes\n" +"-R, --start-delay=# le délai pour le démarrage PCM automtique est de \n" +"# microsecondes (relatif à la taille du tampon si\n" +"<= 0)\n" +"-T, --stop-delay=# # microsecondes de délai depuis xrun avant l'arrêt\n" +"automatique du périphérique PCM \n" +"-v, --verbose afficher la structure et la configuration du\n" +"périphérique PCM (accumulatif)\n" +"-V, --vumeter=TYPE activer le VU-mètre (TYPE : mono ou stéréo)\n" +"-I, --separate-channels un fichier par canal\n" +"-i, --interactive permet une opération interactive depuis stdin\n" +"-m, --chmap=ch1,ch2,.. donne la carte du canal à écraser ou suivre\n" +" --disable-resample désactiver le rééchantillonage de fréquence\n" +"automatique\n" +" --disable-channels désactiver les conversions automatiques de canal\n" +" --disable-format désactiver les conversions automatiques de format\n" +" --disable-softvol désactiver le contrôle du volume logiciel (softvol)\n" +" --test-position tester la position du tampon circulaire\n" +" --test-coef=# test coefficient pour la position du tampon\n" +"circulaire (par défaut 8). L'expression pour la\n" +"validation est : coef * (taille_tampon / 2)\n" +" --test-nowait ne pas attendre le tampon circulaire - prend toutes\n" +"les resources du processeur\n" +" --max-file-time=# commencer un autre fichier de capture quand l'ancien\n" +"fichier a enregistré pendant X secondes\n" +" --process-id-file écrire l'identifiant du processus ici\n" +" --use-strftime utiliser strftime pour le nom du fichier de sortie\n" +" --dump-hw-params récupérer le hw_params du périphérique\n" +" --fatal-errors traiter toutes les erreurs comme fatales\n" + +#: aplay/aplay.c:248 speaker-test/speaker-test.c:1023 +#, c-format +msgid "Recognized sample formats are:" +msgstr "Les formats d'échantillonage connus sont :" + +#: aplay/aplay.c:254 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"Certains de ceux-ci ne sont peut être pas disponibles avec le matériel sélectionné\n" + +#: aplay/aplay.c:255 +#, c-format +msgid "The available format shortcuts are:\n" +msgstr "Les raccourcis format disponibles sont :\n" + +#: aplay/aplay.c:256 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16 bit petit boutiste, 44100, stéréo)\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16 bit gros boutiste, 44100, stéréo)\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16 bit petit boutiste, 48000, stéréo)\n" + +#: aplay/aplay.c:272 +msgid "no soundcards found..." +msgstr "aucune carte son n'a été trouvée..." + +#: aplay/aplay.c:275 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** Liste des périphériques matériels %s ****\n" + +#: aplay/aplay.c:304 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "carte %i : %s [%s], périphérique %i : %s [%s]\n" + +#: aplay/aplay.c:310 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " Sous-périphériques : %i/%i\n" + +#: aplay/aplay.c:317 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " Sous-périphérique #%i : %s\n" + +#: aplay/aplay.c:398 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "Interrompu par le signal %s...\n" + +#: aplay/aplay.c:553 +msgid "command should be named either arecord or aplay" +msgstr "la commande doit être nommée arecord ou aplay" + +#: aplay/aplay.c:597 +#, c-format +msgid "unrecognized file format %s" +msgstr "format de fichier inconnu %s" + +#: aplay/aplay.c:604 +#, c-format +msgid "invalid channels argument '%s'" +msgstr "argument des canaux invalides '%s'" + +#: aplay/aplay.c:608 +#, c-format +msgid "value %i for channels is invalid" +msgstr "la valeur %i pour les canaux est incorrecte" + +#: aplay/aplay.c:627 +#, c-format +msgid "wrong extended format '%s'" +msgstr "format étendu '%s' erronné" + +#: aplay/aplay.c:635 +#, c-format +msgid "invalid rate argument '%s'" +msgstr "argument de taux invalide '%s'" + +#: aplay/aplay.c:642 +#, c-format +msgid "bad speed value %i" +msgstr "mauvaise valeur de vitesse %i" + +#: aplay/aplay.c:648 +msgid "duration and samples arguments cannot be used together" +msgstr "la durée et les arguments d'échantillons ne peuvent pas être utilisés ensemble" + +#: aplay/aplay.c:653 +#, c-format +msgid "invalid duration argument '%s'" +msgstr "argument de durée invalide '%s'" + +#: aplay/aplay.c:660 +msgid "samples and duration arguments cannot be used together" +msgstr "les échantillons et les arguments de durée ne peuvent pas être utilisés ensemble" + +#: aplay/aplay.c:665 +#, c-format +msgid "invalid samples argument '%s'" +msgstr "argument d'échantillon invalide '%s'" + +#: aplay/aplay.c:677 +#, c-format +msgid "invalid period time argument '%s'" +msgstr "argument de temps de période invalide '%s'" + +#: aplay/aplay.c:684 +#, c-format +msgid "invalid buffer time argument '%s'" +msgstr "argument de temps de tampon invalide '%s'" + +#: aplay/aplay.c:691 +#, c-format +msgid "invalid period size argument '%s'" +msgstr "argument de taille de période invalide '%s'" + +#: aplay/aplay.c:698 +#, c-format +msgid "invalid buffer size argument '%s'" +msgstr "argument de taille de tampon invalide '%s'" + +#: aplay/aplay.c:705 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "argument d'espace disponible minimum invalide '%s'" + +#: aplay/aplay.c:712 +#, c-format +msgid "invalid start delay argument '%s'" +msgstr "argument de délai de départ invalide '%s'" + +#: aplay/aplay.c:719 +#, c-format +msgid "invalid stop delay argument '%s'" +msgstr "argument de délai d'arrêt invalide '%s'" + +#: aplay/aplay.c:774 +#, c-format +msgid "invalid test coef argument '%s'" +msgstr "argument de coef de test invalide '%s'" + +#: aplay/aplay.c:786 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "argument de temps de fichier maximum invalide '%s'" + +#: aplay/aplay.c:806 speaker-test/speaker-test.c:666 +#, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "Impossible d'analyser la chaîne de map du canal : %s\n" + +#: aplay/aplay.c:812 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "Essayez `%s --help' pour plus d'informations.\n" + +#: aplay/aplay.c:828 +#, c-format +msgid "audio open error: %s" +msgstr "erreur à l'ouverture audio : %s" + +#: aplay/aplay.c:833 +#, c-format +msgid "info error: %s" +msgstr "erreur info : %s" + +#: aplay/aplay.c:840 +#, c-format +msgid "nonblock setting error: %s" +msgstr "paramètre de non-blocage erronné : %s" + +#: aplay/aplay.c:850 aplay/aplay.c:974 aplay/aplay.c:1264 aplay/aplay.c:1458 +#: aplay/aplay.c:1979 aplay/aplay.c:2009 +msgid "not enough memory" +msgstr "pas assez de mémoire" + +#: aplay/aplay.c:874 +#, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "Le fichier d'identifiant de processus %s n'a pas pu être créé : %s" + +#: aplay/aplay.c:964 +#, c-format +msgid "read error (called from line %i)" +msgstr "erreur de lecture (appelé à la ligne %i)" + +#: aplay/aplay.c:1031 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "longueur du morceau 'fmt ' inconnue (lu %u, devrait être au moins %u)" + +#: aplay/aplay.c:1042 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"longueur du morceau 'fmt ' extensible inconnue (lu %u, devrait être au moins " +"%u)" + +#: aplay/aplay.c:1047 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "libellé de format erronné dans le morceau extensible 'fmt '" + +#: aplay/aplay.c:1054 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "" +"Les formats de ficher WAVE 0x%04x qui ne sont pas encodés en PCM ou FLOAT ne " +"sont pas supportés" + +#: aplay/aplay.c:1059 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "Les fichiers WAVE avec %d pistes ne sont pas supportés" + +#: aplay/aplay.c:1067 aplay/aplay.c:1193 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "Avertissement : le format est changé en U8\n" + +#: aplay/aplay.c:1077 aplay/aplay.c:1090 aplay/aplay.c:1101 +#, c-format +msgid "Warning: format is changed to %s\n" +msgstr "Avertissement : le format est changé en %s\n" + +#: aplay/aplay.c:1106 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr "" +" les fichiers WAVE échantillonés %d bits par %d octets en largeur (%d " +"canaux) ne sont pas supportés" + +#: aplay/aplay.c:1129 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr "" +" les fichiers WAVES échantillonés %d bits en largeur ne sont pas supportés" + +#: aplay/aplay.c:1187 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "Avertissement : le format est changé en MU_LAW\n" + +#: aplay/aplay.c:1199 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "Avertissement : le format est changé en S16_BE\n" + +#: aplay/aplay.c:1212 aplay/aplay.c:2294 aplay/aplay.c:2301 aplay/aplay.c:2831 +msgid "read error" +msgstr "erreur de lecture" + +#: aplay/aplay.c:1242 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "Les numéros de canal ne correspondent pas entre hw_params et la carte de canal" + +#: aplay/aplay.c:1251 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "Avertissement : impossible d'obtenir la carte de canal\n" + +#: aplay/aplay.c:1284 +#, c-format +msgid "Channel %d doesn't match with hw_parmas" +msgstr "Le canal %d ne correspond pas avec hw_parmas" + +#: aplay/aplay.c:1310 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "Configuration erronée pour ce PCM : pas de configuration disponible" + +#: aplay/aplay.c:1314 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "Params HW pour le périphérique \"%s\" :\n" + +#: aplay/aplay.c:1334 +msgid "Access type not available" +msgstr "Ce type d'accès n'est pas disponible" + +#: aplay/aplay.c:1339 +msgid "Sample format non available" +msgstr "Ce format d'échantillonage n'est pas disponible" + +#: aplay/aplay.c:1345 +msgid "Channels count non available" +msgstr "Nombre de canaux non disponible" + +#: aplay/aplay.c:1360 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "" +"Avertissement : la fréquence n'est pas précise (demandé = %iHz,\n" +"obtenu = %iHz)\n" + +#: aplay/aplay.c:1366 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " veuillez essayez le greffon de branchement %s\n" + +#: aplay/aplay.c:1403 +msgid "Unable to install hw params:" +msgstr "Les paramètres matériel n'ont pas pu être installés :" + +#: aplay/aplay.c:1410 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "" +"Une période égale à la taille du tampon (%lu == %lu) ne peut être utilisée" + +#: aplay/aplay.c:1441 +msgid "unable to install sw params:" +msgstr "Les paramètres logiciel n'ont pas pu être installés :" + +#: aplay/aplay.c:1476 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "problème snd_pcm_mmap_begin : %s" + +#: aplay/aplay.c:1505 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "paramétrage du fanion stdin O_NONBLOCK échoué\n" + +#: aplay/aplay.c:1529 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "commande \rPAUSE ignorée (pas de support hw)\n" + +#: aplay/aplay.c:1537 +#, c-format +msgid "pause push error: %s" +msgstr "erreur de poussée de pause : %s" + +#: aplay/aplay.c:1548 +#, c-format +msgid "pause release error: %s" +msgstr "erreur de la relâche de la pause : %s" + +#: aplay/aplay.c:1564 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" + +#: aplay/aplay.c:1606 +#, c-format +msgid "status error: %s" +msgstr "erreur d'état : %s" + +#: aplay/aplay.c:1611 +#, c-format +msgid "fatal %s: %s" +msgstr "fatal %s : %s" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1626 aplay/aplay.c:1634 +msgid "underrun" +msgstr "perte" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1634 +msgid "overrun" +msgstr "dépassement" + +#: aplay/aplay.c:1622 aplay/aplay.c:1633 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (au moins longue de %.3f ms)\n" + +#: aplay/aplay.c:1638 +#, c-format +msgid "Status:\n" +msgstr "État :\n" + +#: aplay/aplay.c:1642 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "xrun : erreur de préparation : %s" + +#: aplay/aplay.c:1648 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "Statut (DRAINAGE) :\n" + +#: aplay/aplay.c:1652 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "" +"changement de format de flux de capture ? tentative de récupération...\n" + +#: aplay/aplay.c:1654 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "xrun(DRAINAGE) : erreur de préparation : %s" + +#: aplay/aplay.c:1661 +#, c-format +msgid "Status(R/W):\n" +msgstr "Status(R/W) :\n" + +#: aplay/aplay.c:1664 +#, c-format +msgid "read/write error, state = %s" +msgstr "erreur de lecture/écriture, état = %s" + +#: aplay/aplay.c:1674 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "Suspendu. Tentative de reprise. " + +#: aplay/aplay.c:1679 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "Échec. Redémarrage du flux." + +#: aplay/aplay.c:1681 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "suspension : prepare error : %s" + +#: aplay/aplay.c:1686 +#, c-format +msgid "Done.\n" +msgstr "Terminé.\n" + +#: aplay/aplay.c:1708 +#, c-format +msgid " !clip " +msgstr "" + +#: aplay/aplay.c:1855 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "%d bit(s) non supporté(s).\n" + +#: aplay/aplay.c:1889 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "Pic max. (%li échantillons) : 0x%08x " + +#: aplay/aplay.c:1923 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "" +"Position de tampon suspicieuse (total %li) : dispo = %li, délai = %li, tampon " +"= %li\n" + +#: aplay/aplay.c:2051 +#, c-format +msgid "write error: %s" +msgstr "erreur en écriture : %s" + +#: aplay/aplay.c:2099 +#, c-format +msgid "writev error: %s" +msgstr "erreur de writev : %s" + +#: aplay/aplay.c:2143 +#, c-format +msgid "read error: %s" +msgstr "erreur en lecture : %s" + +#: aplay/aplay.c:2187 +#, c-format +msgid "readv error: %s" +msgstr "erreur de readv : %s" + +#: aplay/aplay.c:2235 +msgid "can't allocate buffer for silence" +msgstr "Un tampon pour silence n'a pas pu être alloué" + +#: aplay/aplay.c:2244 aplay/aplay.c:2472 aplay/aplay.c:2477 aplay/aplay.c:2527 +#: aplay/aplay.c:2536 aplay/aplay.c:2543 aplay/aplay.c:2553 aplay/aplay.c:2559 +#: aplay/aplay.c:2631 aplay/aplay.c:2661 aplay/aplay.c:2675 +msgid "write error" +msgstr "erreur en écriture" + +#: aplay/aplay.c:2257 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "voc_pcm_flush - erreur de silence" + +#: aplay/aplay.c:2260 +msgid "voc_pcm_flush error" +msgstr "erreur voc_pcm_flush" + +#: aplay/aplay.c:2285 +msgid "malloc error" +msgstr "erreur malloc" + +#: aplay/aplay.c:2289 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "Lecture du ficher de canaux Creative Labs '%s'...\n" + +#: aplay/aplay.c:2357 aplay/aplay.c:2452 +msgid "can't play packed .voc files" +msgstr "les fichiers .voc de type 'packed' ne sont pas supportés" + +#: aplay/aplay.c:2412 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "%s n'est pas navigable; les motifs n'ont pas pu être lus\n" + +#: aplay/aplay.c:2461 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "type de bloc %d inconnu. fin." + +#: aplay/aplay.c:2595 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "Le format wave %s n'est pas supporté..." + +#: aplay/aplay.c:2655 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "Le format %s Sparc Audio n'est pas supporté..." + +#: aplay/aplay.c:2736 +msgid "Playing" +msgstr "Lecture" + +#: aplay/aplay.c:2736 +msgid "Recording" +msgstr "Capture" + +#: aplay/aplay.c:2740 +#, c-format +msgid "Rate %d Hz, " +msgstr "Fréquence %d Hz, " + +#: aplay/aplay.c:2742 +#, c-format +msgid "Mono" +msgstr "" + +#: aplay/aplay.c:2744 +#, c-format +msgid "Stereo" +msgstr "Stéréo" + +#: aplay/aplay.c:2746 +#, c-format +msgid "Channels %i" +msgstr "%i Canaux" + +#: aplay/aplay.c:3344 aplay/aplay.c:3397 +#, c-format +msgid "You need to specify %d files" +msgstr "Vous devez spécifier %d fichiers" + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - gestionnaire de connection séquenceur ALSA\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "Utilisation :\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * Connexion/déconnexion entre deux ports\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [-options] envoyeur destinataire\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " envoyeur, destinataire = couple client:port\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect déconnecter\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive connexion exclusive\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # convertit real-time-stamp à la queue\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # convertit tick-time-stamp à la queue\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * Lister les ports connectés (pas de souscription)\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr "" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr " -i,--input lister les ports (lisibles) d'entrée\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr " -o,--output lister les ports (écrivables) de sortie\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr "" +" -l,--list lister les connexions en cours\n" +" de chaque port\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr " * Retirer toutes les connexions exportées\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr "" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "Connexion à" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "Connecté depuis" + +#: seq/aconnect/aconnect.c:171 +#, c-format +msgid "client %d: '%s' [type=%s" +msgstr "client %d : '%s' [type=%s" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "utilisateur" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "noyau" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "le séquenceur n'a pas pu être ouvert\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "l'identifiant du client n'as pas pu être obtenu\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "les infos du client n'ont pas pu être définies\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "l'adresse envoyeur %s est incorrecte\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:290 +#, c-format +msgid "invalid destination address %s\n" +msgstr "l'adresse destinataire %s est incorrecte\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "Aucune souscription trouvée\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "La déconnexion a échoué (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "La connexion a déjà été souscrite\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "Échec de connexion (%s)\n" + +#: seq/aseqnet/aseqnet.c:164 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - client/serveur réseau sur le séquenceur ALSA\n" + +#: seq/aseqnet/aseqnet.c:165 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr "" + +#: seq/aseqnet/aseqnet.c:166 +#, c-format +msgid "usage:\n" +msgstr "utilisation :\n" + +#: seq/aseqnet/aseqnet.c:167 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " mode serveur : aseqnet [-options]\n" + +#: seq/aseqnet/aseqnet.c:168 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " mode client : aseqnet [-options] hôte_serveur\n" + +#: seq/aseqnet/aseqnet.c:169 +#, c-format +msgid "options:\n" +msgstr "options :\n" + +#: seq/aseqnet/aseqnet.c:170 +#, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : spécifie le port TCP (numéro ou nom de service)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr " -s,--source addr : lire à partir de l'adr. donnée (client:port)\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest addr : écrire à l'adr. donnée (client:port)\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v, --verbose : affichage locace de messages\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i, --info : afficher certains évènements reçus\n" + +#: seq/aseqnet/aseqnet.c:188 +#, c-format +msgid "can't malloc\n" +msgstr "échec allocation mémoire (malloc)\n" + +#: seq/aseqnet/aseqnet.c:213 +#, c-format +msgid "closing files..\n" +msgstr "fermeture des fichiers...\n" + +#: seq/aseqnet/aseqnet.c:272 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "séquenceur ouvert : %d:%d\n" + +#: seq/aseqnet/aseqnet.c:279 +#, c-format +msgid "invalid source address %s\n" +msgstr "adresse source %s incorrecte\n" + +#: seq/aseqnet/aseqnet.c:309 +#, c-format +msgid "service '%s' is not found in /etc/services\n" +msgstr "le service '%s' n'as pas été trouvé dans /etc/services\n" + +#: seq/aseqnet/aseqnet.c:377 +#, c-format +msgid "too many connections!\n" +msgstr "trop de connexions !\n" + +#: seq/aseqnet/aseqnet.c:388 +#, c-format +msgid "accepted[%d]\n" +msgstr "accepté[%d]\n" + +#: seq/aseqnet/aseqnet.c:411 +#, c-format +msgid "can't get address %s\n" +msgstr "l'adresse %s na pas pu être obtenue\n" + +#: seq/aseqnet/aseqnet.c:422 +#, c-format +msgid "ok.. connected\n" +msgstr "ok.. connecté\n" + +#: seq/aseqnet/aseqnet.c:518 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "Canal %2d : évènement de contrôle : %5d\n" + +#: seq/aseqnet/aseqnet.c:522 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "Canal %2d : Pitchbender : %5d\n" + +#: seq/aseqnet/aseqnet.c:526 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "Canal %2d : évènement Note On : %5d\n" + +#: seq/aseqnet/aseqnet.c:530 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "Canal %2d : évènement Note Off : %5d\n" + +#: seq/aseqnet/aseqnet.c:585 +#, c-format +msgid "disconnected\n" +msgstr "déconnecté\n" + +#: speaker-test/speaker-test.c:121 +msgid "Front Left" +msgstr "Avant gauche" + +#: speaker-test/speaker-test.c:122 +msgid "Front Right" +msgstr "Avant droit" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Left" +msgstr "Arrière gauche" + +#: speaker-test/speaker-test.c:124 +msgid "Rear Right" +msgstr "Arrière droit" + +#: speaker-test/speaker-test.c:126 +msgid "LFE" +msgstr "" + +#: speaker-test/speaker-test.c:127 +msgid "Side Left" +msgstr "Côté gauche" + +#: speaker-test/speaker-test.c:128 +msgid "Side Right" +msgstr "Côté droit" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 9" +msgstr "Canal 9" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 10" +msgstr "Canal 10" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 11" +msgstr "Canal 11" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 12" +msgstr "Canal 12" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 13" +msgstr "Canal 13" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 14" +msgstr "Canal 14" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 15" +msgstr "Canal 15" + +#: speaker-test/speaker-test.c:136 +msgid "Channel 16" +msgstr "Canal 16" + +#: speaker-test/speaker-test.c:519 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "Configuration erronée pour la lecture : pas de configuration disponible : %s\n" + +#: speaker-test/speaker-test.c:526 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "Le type d'accès est indisponible à la lecture : %s\n" + +#: speaker-test/speaker-test.c:533 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "Le format d'échantillonage est indisponible à la lecture : %s\n" + +#: speaker-test/speaker-test.c:540 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "Le nombre (%i) de canaux est indisponible à la lecture : %s\n" + +#: speaker-test/speaker-test.c:548 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "La fréquence %iHz est indisponible à la lecture : %s\n" + +#: speaker-test/speaker-test.c:553 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "" +"La fréquence ne correspond pas (demandée : %iHz, obtenue %iHz, err %d)\n" + +#: speaker-test/speaker-test.c:557 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "La fréquence est %iHz (demandée %iHz)\n" + +#: speaker-test/speaker-test.c:563 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "L'intervalle de la taille du tampon est de %lu à %lu\n" + +#: speaker-test/speaker-test.c:564 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "L'intervalle de la taille de la période est de %lu à %lu\n" + +#: speaker-test/speaker-test.c:566 +#, c-format +msgid "Requested period time %u us\n" +msgstr "Temps de période demandé %u µs\n" + +#: speaker-test/speaker-test.c:569 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "Le temps de période %u µs n'a pas pu être défini pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:575 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "Temps tampon demandé %u µs\n" + +#: speaker-test/speaker-test.c:578 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "Le temps tampon de %u µs n'a pas pu être définie pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:587 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "Taille max. de tampon %lu utilisée\n" + +#: speaker-test/speaker-test.c:590 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "La taile de tampon %lu n'a pas pu être définie pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:596 +#, c-format +msgid "Periods = %u\n" +msgstr "Périodes = %u\n" + +#: speaker-test/speaker-test.c:599 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "nperiods %u n'a pas pu être défini pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:608 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "" +"Les paramètres matériel n'ont pas pu être définis pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:614 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "était paramétré pour period_size = %lu\n" + +#: speaker-test/speaker-test.c:615 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "était paramétré pour buffer_size = %lu\n" + +#: speaker-test/speaker-test.c:617 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "tampon trop petit, il n'a pas pu être utilisé\n" + +#: speaker-test/speaker-test.c:630 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "" +"Les paramètres logiciels actuels n'ont pas pu être déterminés pour la " +"lecture : %s\n" + +#: speaker-test/speaker-test.c:637 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "Impossible de paramétrer le mode de déclenchement de démarrage pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:644 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "Impossible de paramétrer le minimum disponible pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:651 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "" +"Les paramètres logiciel n'ont pas pu être définis pour la lecture : %s\n" + +#: speaker-test/speaker-test.c:671 +#, c-format +msgid "Unable to set channel map: %s\n" +msgstr "Impossible de paramètrer la carte de canal : %s\n" + +#: speaker-test/speaker-test.c:696 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "Impossible de récupérer depuis une perte, préparation échouée : %s\n" + +#: speaker-test/speaker-test.c:707 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "N'a pas pu récupérer depuis suspend, préparation échouée : %s\n" + +#: speaker-test/speaker-test.c:771 speaker-test/speaker-test.c:1272 +#, c-format +msgid "No enough memory\n" +msgstr "Pas assez de mémoire\n" + +#: speaker-test/speaker-test.c:776 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "Le fichier WAV %s n'a pas pu être ouvert\n" + +#: speaker-test/speaker-test.c:780 speaker-test/speaker-test.c:809 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "Fichier WAV incorrect %s\n" + +#: speaker-test/speaker-test.c:785 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "Le fichier %s n'est pas au format WAV\n" + +#: speaker-test/speaker-test.c:789 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "Format WAV %d non supporté pour %s\n" + +#: speaker-test/speaker-test.c:794 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s n'est pas un flux en mono (%d canaux)\n" + +#: speaker-test/speaker-test.c:799 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "La fréquence d'échantillonage (%d) ne correspond pas à %s\n" + +#: speaker-test/speaker-test.c:804 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "Le format d'échantillonage %d bits n'est pas supporté pour %s\n" + +#: speaker-test/speaker-test.c:865 +#, c-format +msgid "Undefined channel %d\n" +msgstr "Canal non-défini %d\n" + +#: speaker-test/speaker-test.c:916 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "Erreur en écriture : %d,%s\n" + +#: speaker-test/speaker-test.c:918 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery échoué : %d,%s\n" + +#: speaker-test/speaker-test.c:1004 +#, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"Utilisation : speaker-test [OPTION]... \n" +"-h,--help\taide\n" +"-D,--device\tpériphérique de lecture\n" +"-r,--rate\tfréquence du flux en Hz\n" +"-c,--channels\tnombre de canaux du flux\n" +"-f,--frequency\tfréquence d'onde sinus en Hz\n" +"-F,--format\tformat d'échantillonage\n" +"-b,--buffer\ttaille en µs du tampon circulaire\n" +"-p,--period\ttaille de la période en µs\n" +"-P,--nperiods\tnombre de périodes\n" +"-t,--test\tpink=bruit rose, sine=courbe sinus, wav=fichier WAV\n" +"-l,--nloops\tspécifier le nombre de boucles à tester, 0 = infini\n" +"-s,--speaker\ttest sur une seule enceinte.\n" +"\t\tValeurs 1=Gauche, 2=Droite, etc.\n" +"-w,--wavfile\tUtiliser le fichier WAV spécifié pour le test sonore\n" +"-w,--wavdir\tSpécifier le répertoire contenant les fichiers WAV\n" +"-m,--chmap\tSpécifier la carte de canal à écraser\n" +"-X,--force-frequency\tpermettre fréquences en dehors de la gamme 30-8000Hz\n" +"-S,--scale\tÉchelle des tonalités de test générées en pourcent (défaut=80)\n" +"\n" + +#: speaker-test/speaker-test.c:1136 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "Nombre de périodes %d incorrect\n" + +#: speaker-test/speaker-test.c:1152 speaker-test/speaker-test.c:1156 +#, c-format +msgid "Invalid test type %s\n" +msgstr "Type de test %s incorrect\n" + +#: speaker-test/speaker-test.c:1189 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "Option '%c' inconnue\n" + +#: speaker-test/speaker-test.c:1203 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "Paramètre incorrect pour l'option -s.\n" + +#: speaker-test/speaker-test.c:1218 +#, c-format +msgid "Playback device is %s\n" +msgstr "Le périphérique de lecture est %s\n" + +#: speaker-test/speaker-test.c:1219 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "Les paramètres du flux sont %iHz, %s, %i canaux\n" + +#: speaker-test/speaker-test.c:1222 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "Utilisation de 16 octaves de bruit rose\n" + +#: speaker-test/speaker-test.c:1225 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "La fréquence de la courbe sinus est %.4fHz\n" + +#: speaker-test/speaker-test.c:1228 +#, c-format +msgid "WAV file(s)\n" +msgstr "fichier(s) WAV\n" + +#: speaker-test/speaker-test.c:1238 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "Erreur d'ouverture à la lecture : %d,%s\n" + +#: speaker-test/speaker-test.c:1244 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "Échec de la configuration des paramètres matériel : %s\n" + +#: speaker-test/speaker-test.c:1248 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "Échec de la configuration des paramètres logiciel : %s\n" + +#: speaker-test/speaker-test.c:1295 speaker-test/speaker-test.c:1317 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "Échec du transfer : %s\n" + +#: speaker-test/speaker-test.c:1303 +#, c-format +msgid "Time per period = %lf\n" +msgstr "Temps par période = %lf\n" + +#~ msgid "Warning: format is changed to S16_LE\n" +#~ msgstr "Avertissement : le format est changé en S16_LE\n" + +#~ msgid "Warning: format is changed to S24_3LE\n" +#~ msgstr "Avertissement : le format est changé en S24_3LE\n" + +#~ msgid "Warning: format is changed to S24_LE\n" +#~ msgstr "Avertissement : le format est changé en S24_LE\n" diff --git a/po/ja.po b/po/ja.po new file mode 100644 index 0000000..6e35088 --- /dev/null +++ b/po/ja.po @@ -0,0 +1,1678 @@ +# Japanese translations for alsa-utils package +# alsa-utils パッケージã«å¯¾ã™ã‚‹è‹±è¨³. +# Copyright (C) 2005 Free Software Foundation, Inc. +# This file is distributed under the same license as the alsa-utils package. +# Takashi Iwai , 2005. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.9a\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-10 09:07+0100\n" +"PO-Revision-Date: 2009-05-27 15:08+0200\n" +"Last-Translator: Takashi Iwai \n" +"Language-Team: Japanese\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: alsamixer/card_select.c:126 alsamixer/device_name.c:126 +msgid "Sound Card" +msgstr "サウンドカード" + +#: alsamixer/card_select.c:181 +msgid "(default)" +msgstr "(デフォルト)" + +#: alsamixer/card_select.c:191 +msgid "cannot enumerate sound cards" +msgstr "サウンドカードを検出ã§ãã¾ã›ã‚“" + +#: alsamixer/card_select.c:215 +msgid "enter device name..." +msgstr "デãƒã‚¤ã‚¹å入力..." + +#: alsamixer/cli.c:40 +msgid "Usage: alsamixer [options]" +msgstr "使用法: alsamixer [オプション]" + +#: alsamixer/cli.c:41 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"主ãªã‚ªãƒ—ション:\n" +" -h, --help ã“ã®ãƒ˜ãƒ«ãƒ—ç”»é¢\n" +" -c, --card=ç•ªå· ã‚µã‚¦ãƒ³ãƒ‰ã‚«ãƒ¼ãƒ‰ç•ªå·ã¾ãŸã¯ID\n" +" -D, --device=åå‰ ãƒŸã‚­ã‚µãƒ¼ãƒ‡ãƒã‚¤ã‚¹å\n" +" -V, --view=表示モード 開始時ã®è¡¨ç¤ºãƒ¢ãƒ¼ãƒ‰: playback/capture/all" + +#: alsamixer/cli.c:46 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"デãƒãƒƒã‚°ã‚ªãƒ—ション:\n" +" -g, --no-color カラー/モノクロ表示\n" +" -a, --abstraction=åå‰ ãƒŸã‚­ã‚µãƒ¼æŠ½è±¡ãƒ¬ãƒ™ãƒ«: none/basic" + +#: alsamixer/cli.c:77 +#, c-format +msgid "invalid card index: %s\n" +msgstr "䏿­£ãªã‚«ãƒ¼ãƒ‰ç•ªå·: %s\n" + +#: alsamixer/cli.c:103 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "æœªçŸ¥ã®æŠ½è±¡ãƒ¬ãƒ™ãƒ«: %s\n" + +#: alsamixer/cli.c:108 +#, c-format +msgid "unknown option: %c\n" +msgstr "未知ã®ã‚ªãƒ—ション '%c'\n" + +#: alsamixer/cli.c:110 +msgid "try `alsamixer --help' for more information\n" +msgstr "より詳ã—ã„æƒ…å ±ã¯ã€Œalsamixer --helpã€ã‚’実行ã—ã¦ãã ã•ã„\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "デãƒã‚¤ã‚¹å:" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "" + +#: alsamixer/mixer_display.c:98 +msgid "Card:" +msgstr "カード:" + +#: alsamixer/mixer_display.c:99 +msgid "Chip:" +msgstr "ãƒãƒƒãƒ—:" + +#: alsamixer/mixer_display.c:100 +msgid "View:" +msgstr "表示:" + +#: alsamixer/mixer_display.c:101 +msgid "Item:" +msgstr "é …ç›®:" + +#: alsamixer/mixer_display.c:104 +msgid "F1: Help" +msgstr "F1: ヘルプ" + +#: alsamixer/mixer_display.c:105 +msgid "F2: System information" +msgstr "F2: システム情報" + +#: alsamixer/mixer_display.c:106 +msgid "F6: Select sound card" +msgstr "F6: ã‚µã‚¦ãƒ³ãƒ‰ã‚«ãƒ¼ãƒ‰é¸æŠž" + +#: alsamixer/mixer_display.c:107 +msgid "Esc: Exit" +msgstr "Esc: 終了" + +#: alsamixer/mixer_display.c:174 +msgid "(unplugged)" +msgstr "" + +#: alsamixer/mixer_display.c:192 +msgid "Playback" +msgstr "å†ç”Ÿ" + +#: alsamixer/mixer_display.c:193 +msgid "Capture" +msgstr "録音" + +#: alsamixer/mixer_display.c:194 +msgid "All" +msgstr "å…¨ã¦" + +#: alsamixer/mixer_display.c:234 +msgid "mute" +msgstr "ミュート" + +#: alsamixer/mixer_display.c:275 alsamixer/mixer_display.c:285 +msgid "dB gain:" +msgstr "dBゲイン:" + +#: alsamixer/mixer_display.c:285 +#, c-format +msgid " [%s %s, %s]" +msgstr "" + +#: alsamixer/mixer_display.c:294 alsamixer/mixer_display.c:300 +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:312 +msgid "Off" +msgstr "オフ" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:312 +msgid "On" +msgstr "オン" + +#: alsamixer/mixer_display.c:363 +msgid "The sound device was unplugged." +msgstr "デãƒã‚¤ã‚¹ãŒæŽ¥ç¶šã•れã¦ã„ã¾ã›ã‚“" + +#: alsamixer/mixer_display.c:364 +msgid "Press F6 to select another sound card." +msgstr "ä»–ã®ã‚«ãƒ¼ãƒ‰ã‚’é¸æŠžã™ã‚‹ã«ã¯F6を押ã—ã¦ä¸‹ã•ã„" + +#: alsamixer/mixer_display.c:379 +msgid "This sound device does not have any playback controls." +msgstr "ã“ã®ã‚«ãƒ¼ãƒ‰ã«ã¯å†ç”ŸãƒŸã‚­ã‚µãƒ¼ãŒã‚りã¾ã›ã‚“" + +#: alsamixer/mixer_display.c:381 +msgid "This sound device does not have any capture controls." +msgstr "ã“ã®ã‚«ãƒ¼ãƒ‰ã«ã¯éŒ²éŸ³ãƒŸã‚­ã‚µãƒ¼ãŒã‚りã¾ã›ã‚“" + +#: alsamixer/mixer_display.c:383 +msgid "This sound device does not have any controls." +msgstr "ã“ã®ã‚«ãƒ¼ãƒ‰ã«ã¯åˆ¶å¾¡å¯èƒ½ãªãƒŸã‚­ã‚µãƒ¼ãŒã‚りã¾ã›ã‚“" + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:516 alsamixer/mixer_display.c:521 +msgid "O" +msgstr "" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:518 alsamixer/mixer_display.c:522 +msgid "M" +msgstr "" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:536 +msgid "L" +msgstr "" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:540 +msgid "R" +msgstr "" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:542 +msgid "CAPTURE" +msgstr "録音" + +#: alsamixer/mixer_display.c:592 +msgid "Front" +msgstr "フロント" + +#: alsamixer/mixer_display.c:595 +msgid "Rear" +msgstr "リア" + +#: alsamixer/mixer_display.c:598 speaker-test/speaker-test.c:125 +msgid "Center" +msgstr "センター" + +#: alsamixer/mixer_display.c:601 +msgid "Woofer" +msgstr "低音" + +#: alsamixer/mixer_display.c:604 +msgid "Side" +msgstr "サイド" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "ミキサーを開ã‘ã¾ã›ã‚“" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "ミキサーをロードã§ãã¾ã›ã‚“" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "ミキサーデãƒã‚¤ã‚¹'%s'ã‚’é–‹ã‘ã¾ã›ã‚“" + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc 終了" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H ヘルプ" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / システム情報" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 å†ç”ŸãƒŸã‚­ã‚µãƒ¼è¡¨ç¤º" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 録音ミキサー表示" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 å…¨ã¦è¡¨ç¤º" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab 表示モード変更 (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F5 S ã‚µã‚¦ãƒ³ãƒ‰ã‚«ãƒ¼ãƒ‰é¸æŠž" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L ç”»é¢å†æç”»" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "↠å‰ã®é …ç›®ã«ç§»ã‚‹" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "→ 次ã®é …ç›®ã«ç§»ã‚‹" + +# ↑ +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "↑/↓ 音é‡å¤‰æ›´" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - 音é‡å¤‰æ›´" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "Page Up/dn 音é‡å¤‰æ›´" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "End éŸ³é‡ 0%" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 éŸ³é‡ 0%-90%" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E å·¦/両/å³éŸ³é‡ã‚¢ãƒƒãƒ—" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Z X C å·¦/両/å³éŸ³é‡ãƒ€ã‚¦ãƒ³" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B å·¦å³éŸ³é‡ã‚’å¹³å‡åŒ–ã™ã‚‹" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M ミュートをトグル" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr "< > å·¦/å³ãƒŸãƒ¥ãƒ¼ãƒˆã‚’トグル" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "スペース 録音をトグル" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "; ' å·¦/å³éŒ²éŸ³ã‚’トグル" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "作者:" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr "" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr "" + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr "" + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "ヘルプ" + +#: alsamixer/proc_files.c:103 +msgid "Select File" +msgstr "ãƒ•ã‚¡ã‚¤ãƒ«é¸æŠž" + +#: alsamixer/textbox.c:52 alsamixer/textbox.c:66 +msgid "Error" +msgstr "エラー" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "\"%s\"をオープンã§ãã¾ã›ã‚“" + +#: aplay/aplay.c:178 +msgid "raw data" +msgstr "raw データ" + +#: aplay/aplay.c:179 +msgid "VOC" +msgstr "VOC" + +#: aplay/aplay.c:181 +msgid "WAVE" +msgstr "WAVE" + +#: aplay/aplay.c:182 +msgid "Sparc Audio" +msgstr "Sparc オーディオ" + +#: aplay/aplay.c:203 +#, fuzzy, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"使用法: %s [オプション]... [ファイル]...\n" +"\n" +"-h, --help ヘルプ\n" +" --version ç¾åœ¨ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’表示\n" +"-l, --list-devices å…¨ã¦ã®ã‚µã‚¦ãƒ³ãƒ‰ã‚«ãƒ¼ãƒ‰ã¨ã‚ªãƒ¼ãƒ‡ã‚£ã‚ªãƒ‡ãƒã‚¤ã‚¹ã‚’表示\n" +"-L, --list-pcms å…¨ã¦ã®å®šç¾©ã•れ㟠PCM を表示\n" +"-D, --device=NAME 使用ã™ã‚‹ PCM を指定\n" +"-q, --quiet quiet モード\n" +"-t, --file-type TYPE ファイルタイプ (voc, wav, raw ã¾ãŸã¯ au)\n" +"-c, --channels=# ãƒãƒ£ãƒãƒ«æ•°\n" +"-f, --format=FORMAT サンプルフォーマット (大ï¼å°æ–‡å­—区別)\n" +"-r, --rate=# サンプルレート\n" +"-d, --duration=# 指定ã®ç§’数後ã«çµ‚了\n" +"-M, --mmap mmap ストリーム\n" +"-N, --nonblock éžãƒ–ロックモード\n" +"-F, --period-time=# 割り込ã¿é–“éš”ã‚’Î¼ç§’ã§æŒ‡å®š\n" +"-B, --buffer-time=# ãƒãƒƒãƒ•ã‚¡ã®å¤§ãã•ã‚’Î¼ç§’ã§æŒ‡å®š\n" +" --period-size=# 割り込ã¿é–“éš”ã‚’ãƒ•ãƒ¬ãƒ¼ãƒ æ•°ã§æŒ‡å®š\n" +" --buffer-size=# ãƒãƒƒãƒ•ã‚¡ã®å¤§ãã•ã‚’ãƒ•ãƒ¬ãƒ¼ãƒ æ•°ã§æŒ‡å®š\n" +"-A, --avail-min=# wakeup ã®æœ€å°‘ã‚¹ãƒšãƒ¼ã‚¹ã‚’Î¼ç§’ã§æŒ‡å®š\n" +"-R, --start-delay=# 指定ã®Î¼ç§’後㫠PCM を自動スタート\n" +" (0 以下ã®å ´åˆã¯ãƒãƒƒãƒ•ァサイズより)\n" +"-T, --stop-delay=# XRUN ã‹ã‚‰æŒ‡å®šã®Î¼ç§’後㫠PCM ã®è‡ªå‹•åœæ­¢\n" +"-v, --verbose PCM ã®è¨­å®šã‚’表示 (複数指定å¯èƒ½)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels å„ãƒãƒ£ãƒãƒ«ã«ä¸€ã¤ã®ãƒ•ァイルを用ã„ã‚‹\n" +" --disable-resample 自動レート変æ›ã‚’ç¦æ­¢ã™ã‚‹\n" +" --disable-channels 自動ãƒãƒ£ãƒãƒ«å¤‰æ›ã‚’ç¦æ­¢ã™ã‚‹\n" +" --disable-format 自動フォーマット変æ›ã‚’ç¦æ­¢ã™ã‚‹\n" +" --disable-softvol ソフト音é‡åˆ¶å¾¡(softvol)ã‚’ç¦æ­¢ã™ã‚‹\n" +" --test-position リングãƒãƒƒãƒ•ã‚¡ä½ç½®ã‚’テストã™ã‚‹\n" +" --test-coef=# リングãƒãƒƒãƒ•ã‚¡ä½ç½®ãƒ†ã‚¹ãƒˆã®ä¿‚æ•° (デフォルト 8)\n" +" テスト範囲: coef * (buffer_size / 2)\n" +" --test-nowait リングãƒãƒƒãƒ•ã‚¡ã®ã‚¦ã‚¨ã‚¤ãƒˆã‚’ç¦æ­¢ - 注æ„:高CPUè² è·\n" + +#: aplay/aplay.c:248 speaker-test/speaker-test.c:1023 +#, c-format +msgid "Recognized sample formats are:" +msgstr "èªè­˜ã•れるサンプルフォーマット:" + +#: aplay/aplay.c:254 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"ã“れらã®ã„ãã¤ã‹ã¯æŒ‡å®šã®ãƒãƒ¼ãƒ‰ã‚¦ã‚§ã‚¢ã§ä½¿ç”¨ä¸å¯èƒ½ãªå ´åˆãŒã‚りã¾ã™\n" + +#: aplay/aplay.c:255 +#, fuzzy, c-format +msgid "The available format shortcuts are:\n" +msgstr "å¯èƒ½ãªãƒ•ォーマットã®çœç•¥å½¢:\n" + +#: aplay/aplay.c:256 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16 ビットã€ãƒªãƒˆãƒ«ã‚¨ãƒ³ãƒ‡ã‚£ã‚¢ãƒ³ã€44100ã€ã‚¹ãƒ†ãƒ¬ã‚ª)\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16 ビットã€ãƒ“ッグエンディアンã€44100ã€ã‚¹ãƒ†ãƒ¬ã‚ª)\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16 ビットã€ãƒªãƒˆãƒ«ã‚¨ãƒ³ãƒ‡ã‚£ã‚¢ãƒ³ã€48000ã€ã‚¹ãƒ†ãƒ¬ã‚ª)\n" + +#: aplay/aplay.c:272 +msgid "no soundcards found..." +msgstr "サウンドカードãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“..." + +#: aplay/aplay.c:275 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** ãƒãƒ¼ãƒ‰ã‚¦ã‚§ã‚¢ãƒ‡ãƒã‚¤ã‚¹ %s ã®ãƒªã‚¹ãƒˆ ****\n" + +#: aplay/aplay.c:304 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "カード %i: %s [%s], デãƒã‚¤ã‚¹ %i: %s [%s]\n" + +#: aplay/aplay.c:310 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " サブデãƒã‚¤ã‚¹: %i/%i\n" + +#: aplay/aplay.c:317 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " サブデãƒã‚¤ã‚¹ #%i: %s\n" + +#: aplay/aplay.c:398 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "シグナル %s ã§ä¸­æ–­...\n" + +#: aplay/aplay.c:553 +msgid "command should be named either arecord or aplay" +msgstr "arecord ã¾ãŸã¯ aplay コマンドã®ã¿å¯èƒ½" + +#: aplay/aplay.c:597 +#, c-format +msgid "unrecognized file format %s" +msgstr "䏿­£ãªãƒ•ァイルフォーマット %s" + +#: aplay/aplay.c:604 +#, fuzzy, c-format +msgid "invalid channels argument '%s'" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:608 +#, c-format +msgid "value %i for channels is invalid" +msgstr "䏿­£ãªãƒãƒ£ãƒãƒ«æ•° %i" + +#: aplay/aplay.c:627 +#, c-format +msgid "wrong extended format '%s'" +msgstr "䏿­£ãªæ‹¡å¼µãƒ•ォーマット '%s'" + +#: aplay/aplay.c:635 +#, fuzzy, c-format +msgid "invalid rate argument '%s'" +msgstr "䏿­£ãªé€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ %s\n" + +#: aplay/aplay.c:642 +#, c-format +msgid "bad speed value %i" +msgstr "䏿­£ãªãƒ¬ãƒ¼ãƒˆå€¤ %i" + +#: aplay/aplay.c:648 +msgid "duration and samples arguments cannot be used together" +msgstr "" + +#: aplay/aplay.c:653 +#, fuzzy, c-format +msgid "invalid duration argument '%s'" +msgstr "å—信アドレスãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:660 +msgid "samples and duration arguments cannot be used together" +msgstr "" + +#: aplay/aplay.c:665 +#, fuzzy, c-format +msgid "invalid samples argument '%s'" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:677 +#, fuzzy, c-format +msgid "invalid period time argument '%s'" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:684 +#, fuzzy, c-format +msgid "invalid buffer time argument '%s'" +msgstr "䏿­£ãªé€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ %s\n" + +#: aplay/aplay.c:691 +#, fuzzy, c-format +msgid "invalid period size argument '%s'" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:698 +#, fuzzy, c-format +msgid "invalid buffer size argument '%s'" +msgstr "䏿­£ãªé€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ %s\n" + +#: aplay/aplay.c:705 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "" + +#: aplay/aplay.c:712 +#, fuzzy, c-format +msgid "invalid start delay argument '%s'" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:719 +#, fuzzy, c-format +msgid "invalid stop delay argument '%s'" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: aplay/aplay.c:774 +#, fuzzy, c-format +msgid "invalid test coef argument '%s'" +msgstr "䏿­£ãªé€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ %s\n" + +#: aplay/aplay.c:786 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "" + +#: aplay/aplay.c:806 speaker-test/speaker-test.c:666 +#, fuzzy, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "avail_min を設定ã§ãã¾ã›ã‚“: %s\n" + +#: aplay/aplay.c:812 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "より詳ã—ã„æƒ…å ±ã¯ã€Œ%s --helpã€ã‚’実行ã—ã¦ãã ã•ã„\n" + +#: aplay/aplay.c:828 +#, c-format +msgid "audio open error: %s" +msgstr "" + +#: aplay/aplay.c:833 +#, c-format +msgid "info error: %s" +msgstr "" + +#: aplay/aplay.c:840 +#, c-format +msgid "nonblock setting error: %s" +msgstr "" + +#: aplay/aplay.c:850 aplay/aplay.c:974 aplay/aplay.c:1264 aplay/aplay.c:1458 +#: aplay/aplay.c:1979 aplay/aplay.c:2009 +msgid "not enough memory" +msgstr "メモリãŒè¶³ã‚Šã¾ã›ã‚“" + +#: aplay/aplay.c:874 +#, fuzzy, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "WAVファイルãŒã‚ªãƒ¼ãƒ—ンã§ãã¾ã›ã‚“: %s\n" + +#: aplay/aplay.c:964 +#, c-format +msgid "read error (called from line %i)" +msgstr "リードエラー (%i 行)" + +#: aplay/aplay.c:1031 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" + +#: aplay/aplay.c:1042 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" + +#: aplay/aplay.c:1047 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "" + +#: aplay/aplay.c:1054 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "" + +#: aplay/aplay.c:1059 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "%d トラックをå«ã‚€ WAVE ファイルã¯å†ç”Ÿã§ãã¾ã›ã‚“" + +#: aplay/aplay.c:1067 aplay/aplay.c:1193 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "警告: フォーマット㯠U8 ã«å¤‰æ›´ã•れã¾ã™\n" + +#: aplay/aplay.c:1077 aplay/aplay.c:1090 aplay/aplay.c:1101 +#, fuzzy, c-format +msgid "Warning: format is changed to %s\n" +msgstr "警告: フォーマット㯠U8 ã«å¤‰æ›´ã•れã¾ã™\n" + +#: aplay/aplay.c:1106 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr "" +"%2$d ãƒã‚¤ãƒˆé•· %1$d サンプルビット (%3$d ãƒãƒ£ãƒãƒ«) ã® WAVE ファイルã¯å†ç”Ÿã§ã" +"ã¾ã›ã‚“" + +#: aplay/aplay.c:1129 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr "%d ビット長ã®ã‚µãƒ³ãƒ—ル㮠WAVE ファイルã¯å†ç”Ÿã§ãã¾ã›ã‚“" + +#: aplay/aplay.c:1187 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "警告: フォーマット㯠MU_LAW ã«å¤‰æ›´ã•れã¾ã™\n" + +#: aplay/aplay.c:1199 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "警告: フォーマット㯠S16_BE ã«å¤‰æ›´ã•れã¾ã™\n" + +#: aplay/aplay.c:1212 aplay/aplay.c:2294 aplay/aplay.c:2301 aplay/aplay.c:2831 +msgid "read error" +msgstr "リードエラー" + +#: aplay/aplay.c:1242 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "" + +#: aplay/aplay.c:1251 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "" + +#: aplay/aplay.c:1284 +#, c-format +msgid "Channel %d doesn't match with hw_parmas" +msgstr "" + +#: aplay/aplay.c:1310 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "指定㮠PCM を使用ã§ãã¾ã›ã‚“: 設定ãŒã‚りã¾ã›ã‚“" + +#: aplay/aplay.c:1314 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "" + +#: aplay/aplay.c:1334 +msgid "Access type not available" +msgstr "アクセスタイプãŒä½¿ç”¨ä¸å¯èƒ½" + +#: aplay/aplay.c:1339 +msgid "Sample format non available" +msgstr "サンプルフォーマットãŒä½¿ç”¨ä¸å¯èƒ½" + +#: aplay/aplay.c:1345 +msgid "Channels count non available" +msgstr "ãƒãƒ£ãƒãƒ«æ•°ãŒä½¿ç”¨ä¸å¯èƒ½" + +#: aplay/aplay.c:1360 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "警告: レートãŒä¸æ­£ç¢ºã§ã™ (è¦æ±‚値 = %iHz, 使用値 = %iHz)\n" + +#: aplay/aplay.c:1366 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " plug プラグイン%s を使用ã—ã¦ãã ã•ã„\n" + +#: aplay/aplay.c:1403 +msgid "Unable to install hw params:" +msgstr "hw params ã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã«å¤±æ•—ã—ã¾ã—ãŸ:" + +#: aplay/aplay.c:1410 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "period 㨠buffer サイズã«ã¯åŒã˜å€¤ã‚’使用ã§ãã¾ã›ã‚“ (%lu == %lu)" + +#: aplay/aplay.c:1441 +msgid "unable to install sw params:" +msgstr "sw params ã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã«å¤±æ•—ã—ã¾ã—ãŸ:" + +#: aplay/aplay.c:1476 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "" + +#: aplay/aplay.c:1505 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "" + +#: aplay/aplay.c:1529 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "" + +#: aplay/aplay.c:1537 +#, fuzzy, c-format +msgid "pause push error: %s" +msgstr "ステータスエラー: %s" + +#: aplay/aplay.c:1548 +#, fuzzy, c-format +msgid "pause release error: %s" +msgstr "サスペンド: prepare エラー: %s" + +#: aplay/aplay.c:1564 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" + +#: aplay/aplay.c:1606 +#, c-format +msgid "status error: %s" +msgstr "ステータスエラー: %s" + +#: aplay/aplay.c:1611 +#, c-format +msgid "fatal %s: %s" +msgstr "" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1626 aplay/aplay.c:1634 +msgid "underrun" +msgstr "アンダーラン" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1634 +msgid "overrun" +msgstr "オーãƒãƒ¼ãƒ©ãƒ³" + +#: aplay/aplay.c:1622 aplay/aplay.c:1633 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (å°‘ãªãã¨ã‚‚ %.3f ms)\n" + +#: aplay/aplay.c:1638 +#, c-format +msgid "Status:\n" +msgstr "ステータス:\n" + +#: aplay/aplay.c:1642 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "" + +#: aplay/aplay.c:1648 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "ステータス(DRAINING):\n" + +#: aplay/aplay.c:1652 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "録音ストリームã®ãƒ•ォーマットãŒå¤‰æ›´ï¼Ÿ 修復を試ã¿ã¾ã™...\n" + +#: aplay/aplay.c:1654 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "" + +#: aplay/aplay.c:1661 +#, c-format +msgid "Status(R/W):\n" +msgstr "ステータス(R/W):\n" + +#: aplay/aplay.c:1664 +#, c-format +msgid "read/write error, state = %s" +msgstr "èª­ã¿æ›¸ãエラー, ステータス = %s" + +#: aplay/aplay.c:1674 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "サスペンド中ã§ã™ã€‚レジュームã—ã¾ã™ã€‚" + +#: aplay/aplay.c:1679 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "失敗ã—ã¾ã—ãŸã€‚ストリームをå†ã‚¹ã‚¿ãƒ¼ãƒˆã—ã¾ã™ã€‚" + +#: aplay/aplay.c:1681 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "サスペンド: prepare エラー: %s" + +#: aplay/aplay.c:1686 +#, c-format +msgid "Done.\n" +msgstr "終了\n" + +#: aplay/aplay.c:1708 +#, c-format +msgid " !clip " +msgstr "" + +#: aplay/aplay.c:1855 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "" + +#: aplay/aplay.c:1889 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "最大ピーク (%li サンプル): 0x%08x " + +#: aplay/aplay.c:1923 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "" + +#: aplay/aplay.c:2051 +#, c-format +msgid "write error: %s" +msgstr "書込エラー: %s" + +#: aplay/aplay.c:2099 +#, c-format +msgid "writev error: %s" +msgstr "書込(writev)エラー: %s" + +#: aplay/aplay.c:2143 +#, c-format +msgid "read error: %s" +msgstr "読込エラー: %s" + +#: aplay/aplay.c:2187 +#, c-format +msgid "readv error: %s" +msgstr "読込(readv)エラー: %s" + +#: aplay/aplay.c:2235 +msgid "can't allocate buffer for silence" +msgstr "サイレンス用ã®ãƒãƒƒãƒ•ã‚¡ã®å–å¾—ã«å¤±æ•—ã—ã¾ã—ãŸ" + +#: aplay/aplay.c:2244 aplay/aplay.c:2472 aplay/aplay.c:2477 aplay/aplay.c:2527 +#: aplay/aplay.c:2536 aplay/aplay.c:2543 aplay/aplay.c:2553 aplay/aplay.c:2559 +#: aplay/aplay.c:2631 aplay/aplay.c:2661 aplay/aplay.c:2675 +msgid "write error" +msgstr "書込エラー" + +#: aplay/aplay.c:2257 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "" + +#: aplay/aplay.c:2260 +msgid "voc_pcm_flush error" +msgstr "" + +#: aplay/aplay.c:2285 +msgid "malloc error" +msgstr "malloc エラー" + +#: aplay/aplay.c:2289 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "Creative Labs Channel ファイル '%s' ã‚’æ¼”å¥ä¸­...\n" + +#: aplay/aplay.c:2357 aplay/aplay.c:2452 +msgid "can't play packed .voc files" +msgstr "packed .voc ãƒ•ã‚¡ã‚¤ãƒ«ã¯æ¼”å¥ã§ãã¾ã›ã‚“" + +#: aplay/aplay.c:2412 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "ループ演å¥ã§ãã¾ã›ã‚“。%s ã¯ã‚·ãƒ¼ã‚¯ä¸å¯èƒ½ã§ã™\n" + +#: aplay/aplay.c:2461 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "未知ã®ãƒ–ロックタイプ %d: 終了ã—ã¾ã™ã€‚" + +#: aplay/aplay.c:2595 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "WAVE 㯠%s フォーマットをサãƒãƒ¼ãƒˆã—ã¾ã›ã‚“..." + +#: aplay/aplay.c:2655 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "Sparc オーディオ㯠%s フォーマットをサãƒãƒ¼ãƒˆã—ã¾ã›ã‚“..." + +#: aplay/aplay.c:2736 +msgid "Playing" +msgstr "å†ç”Ÿä¸­" + +#: aplay/aplay.c:2736 +msgid "Recording" +msgstr "録音中" + +#: aplay/aplay.c:2740 +#, c-format +msgid "Rate %d Hz, " +msgstr "レート %d Hz, " + +#: aplay/aplay.c:2742 +#, c-format +msgid "Mono" +msgstr "モノラル" + +#: aplay/aplay.c:2744 +#, c-format +msgid "Stereo" +msgstr "ステレオ" + +#: aplay/aplay.c:2746 +#, c-format +msgid "Channels %i" +msgstr "ãƒãƒ£ãƒãƒ«æ•° %i" + +#: aplay/aplay.c:3344 aplay/aplay.c:3397 +#, c-format +msgid "You need to specify %d files" +msgstr "%d 個ã®ãƒ•ァイルを指定ã—ã¦ãã ã•ã„" + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - ALSA sequencer 接続マãƒãƒ¼ã‚¸ãƒ£\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "使用法:\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * 二ã¤ã®ãƒãƒ¼ãƒˆã®æŽ¥ç¶šï¼åˆ‡ã‚Šé›¢ã—\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [-オプション] é€ä¿¡ å—ä¿¡\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " é€ä¿¡, å—ä¿¡ = クライアント:ãƒãƒ¼ãƒˆã®çµ„\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect 切り離ã—\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive 排他接続\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # キューã®ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã‚¹ã‚¿ãƒ³ãƒ—ã«å¤‰æ›\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # キューã®ãƒ†ã‚£ãƒƒã‚¯ã‚¿ã‚¤ãƒ ã‚¹ã‚¿ãƒ³ãƒ—ã«å¤‰æ›\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * 接続済ã¿ã®ãƒãƒ¼ãƒˆã®ä¸€è¦§\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr " aconnect -i|-o [-オプション]\n" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr " -i,--input 入力 (読込) ãƒãƒ¼ãƒˆã®ä¸€è¦§\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr " -o,--output 主力 (書込) ãƒãƒ¼ãƒˆã®ä¸€è¦§\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr " -l,--list å„ãƒãƒ¼ãƒˆã®ç¾åœ¨ã®æŽ¥ç¶šã‚’表示\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr " * å…¨ã¦ã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã•ã‚ŒãŸæŽ¥ç¶šã‚’åˆ‡ã‚Šé›¢ã™\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr " -x, --removall\n" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "接続先" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "接続元" + +#: seq/aconnect/aconnect.c:171 +#, fuzzy, c-format +msgid "client %d: '%s' [type=%s" +msgstr "クライアント %d: '%s' [タイプ=%s]\n" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "ユーザ" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "カーãƒãƒ«" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "sequencer をオープンã§ãã¾ã›ã‚“\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "クライアント ID ã‚’å–å¾—ã§ãã¾ã›ã‚“\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "クライアント情報をå–å¾—ã§ãã¾ã›ã‚“\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "é€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒä¸æ­£ã§ã™: %s\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:290 +#, c-format +msgid "invalid destination address %s\n" +msgstr "å—信アドレスãŒä¸æ­£ã§ã™: %s\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "接続ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "切り離ã—ã«å¤±æ•— (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "æ—¢ã«æŽ¥ç¶šã•れã¦ã„ã¾ã™\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "接続ã«å¤±æ•— (%s)\n" + +#: seq/aseqnet/aseqnet.c:164 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - ALSA sequencer 上ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆï¼ã‚µãƒ¼ãƒ\n" + +#: seq/aseqnet/aseqnet.c:165 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr "" + +#: seq/aseqnet/aseqnet.c:166 +#, c-format +msgid "usage:\n" +msgstr "使用法:\n" + +#: seq/aseqnet/aseqnet.c:167 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " サーãƒãƒ¢ãƒ¼ãƒ‰: aseqnet [-オプション]\n" + +#: seq/aseqnet/aseqnet.c:168 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " クライアントモード: aseqnet [-オプション] サーãƒãƒ›ã‚¹ãƒˆ\n" + +#: seq/aseqnet/aseqnet.c:169 +#, c-format +msgid "options:\n" +msgstr "オプション:\n" + +#: seq/aseqnet/aseqnet.c:170 +#, fuzzy, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : TCP ãƒãƒ¼ãƒˆã®æŒ‡å®š (æ•°å­—ã¾ãŸã¯ã‚µãƒ¼ãƒ“スå)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr " -s,--source addr : 指定ã®ã‚¢ãƒ‰ãƒ¬ã‚¹(クライアント:ãƒãƒ¼ãƒˆ)ã‹ã‚‰èª­ã¿è¾¼ã‚€\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest addr : 指定ã®ã‚¢ãƒ‰ãƒ¬ã‚¹(クライアント:ãƒãƒ¼ãƒˆ)ã«æ›¸ã込む\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v,--verbose : 冗長メッセージ表示\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i,--info : å—信イベントを表示ã™ã‚‹\n" + +#: seq/aseqnet/aseqnet.c:188 +#, c-format +msgid "can't malloc\n" +msgstr "malloc ã§ãã¾ã›ã‚“\n" + +#: seq/aseqnet/aseqnet.c:213 +#, c-format +msgid "closing files..\n" +msgstr "ファイルを閉ã˜ã¾ã™..\n" + +#: seq/aseqnet/aseqnet.c:272 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "" + +#: seq/aseqnet/aseqnet.c:279 +#, c-format +msgid "invalid source address %s\n" +msgstr "䏿­£ãªé€ä¿¡ã‚¢ãƒ‰ãƒ¬ã‚¹ %s\n" + +#: seq/aseqnet/aseqnet.c:309 +#, c-format +msgid "service '%s' is not found in /etc/services\n" +msgstr "サービス '%s' ㌠/etc/services ã«è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“\n" + +#: seq/aseqnet/aseqnet.c:377 +#, c-format +msgid "too many connections!\n" +msgstr "接続ãŒå¤šã™ãŽã¾ã™!\n" + +#: seq/aseqnet/aseqnet.c:388 +#, c-format +msgid "accepted[%d]\n" +msgstr "了解[%d]\n" + +#: seq/aseqnet/aseqnet.c:411 +#, c-format +msgid "can't get address %s\n" +msgstr "アドレス %s ã‚’å–å¾—ã§ãã¾ã›ã‚“\n" + +#: seq/aseqnet/aseqnet.c:422 +#, c-format +msgid "ok.. connected\n" +msgstr "ok.. 接続\n" + +#: seq/aseqnet/aseqnet.c:518 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "ãƒãƒ£ãƒãƒ« %2d: コントロール : %5d\n" + +#: seq/aseqnet/aseqnet.c:522 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "ãƒãƒ£ãƒãƒ« %2d: ピッãƒãƒ™ãƒ³ãƒ‰ : %5d\n" + +#: seq/aseqnet/aseqnet.c:526 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "ãƒãƒ£ãƒãƒ« %2d: ノートオン : %5d\n" + +#: seq/aseqnet/aseqnet.c:530 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "ãƒãƒ£ãƒãƒ« %2d: ノートオフ : %5d\n" + +#: seq/aseqnet/aseqnet.c:585 +#, c-format +msgid "disconnected\n" +msgstr "切り離ã—\n" + +#: speaker-test/speaker-test.c:121 +msgid "Front Left" +msgstr "" + +#: speaker-test/speaker-test.c:122 +msgid "Front Right" +msgstr "" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Left" +msgstr "" + +#: speaker-test/speaker-test.c:124 +msgid "Rear Right" +msgstr "" + +#: speaker-test/speaker-test.c:126 +msgid "LFE" +msgstr "" + +#: speaker-test/speaker-test.c:127 +msgid "Side Left" +msgstr "" + +#: speaker-test/speaker-test.c:128 +msgid "Side Right" +msgstr "" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 9" +msgstr "" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 10" +msgstr "" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 11" +msgstr "" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 12" +msgstr "" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 13" +msgstr "" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 14" +msgstr "" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 15" +msgstr "" + +#: speaker-test/speaker-test.c:136 +msgid "Channel 16" +msgstr "" + +#: speaker-test/speaker-test.c:519 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "å†ç”Ÿç”¨ã«è¨­å®šã§ãã¾ã›ã‚“: 設定ãŒã¿ã¤ã‹ã‚Šã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:526 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "アクセスタイプãŒä¸æ­£ã§ã™: %s\n" + +#: speaker-test/speaker-test.c:533 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "指定ã®ã‚µãƒ³ãƒ—ルフォーマットを使用ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:540 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "ãƒãƒ£ãƒãƒ«æ•° (%i) を使用ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:548 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "レート %iHz を使用ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:553 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "設定レートãŒä¸€è‡´ã—ã¾ã›ã‚“< (è¦æ±‚値 %iHz, å–得値 %iHz, エラー %d)\n" + +#: speaker-test/speaker-test.c:557 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "レート %iHz (è¦æ±‚値 %iHz)\n" + +#: speaker-test/speaker-test.c:563 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "ãƒãƒƒãƒ•ァサイズ範囲 %lu 〜 %lu\n" + +#: speaker-test/speaker-test.c:564 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "ピリオドサイズ範囲 %lu 〜 %lu\n" + +#: speaker-test/speaker-test.c:566 +#, c-format +msgid "Requested period time %u us\n" +msgstr "è¦æ±‚ã•れãŸãƒ”リオド長 %u us\n" + +#: speaker-test/speaker-test.c:569 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "ピリオド長 %u us を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:575 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "è¦æ±‚ã•れãŸãƒãƒƒãƒ•ã‚¡é•· %u us\n" + +#: speaker-test/speaker-test.c:578 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "ãƒãƒƒãƒ•ã‚¡é•· %u us を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:587 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "最大ãƒãƒƒãƒ•ァサイズ %lu を使用\n" + +#: speaker-test/speaker-test.c:590 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "ãƒãƒƒãƒ•ァサイズ %lu を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:596 +#, c-format +msgid "Periods = %u\n" +msgstr "ピリオド数 = %u\n" + +#: speaker-test/speaker-test.c:599 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "ピリオド数 %u を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:608 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "hw params を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:614 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "period_size = %lu ã§è¨­å®š\n" + +#: speaker-test/speaker-test.c:615 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "buffer_size = %lu ã§è¨­å®š\n" + +#: speaker-test/speaker-test.c:617 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "ãƒãƒƒãƒ•ã‚¡ãŒå°ã•ã™ãŽã¾ã™\n" + +#: speaker-test/speaker-test.c:630 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "ç¾åœ¨ã® swparams ã‚’å–å¾—ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:637 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "start_threshold モードを設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:644 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "avail_min を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:651 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "å†ç”Ÿç”¨ã® sw params を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:671 +#, fuzzy, c-format +msgid "Unable to set channel map: %s\n" +msgstr "avail_min を設定ã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:696 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "アンダーランã‹ã‚‰å¾©å¸°å¤±æ•—: %s\n" + +#: speaker-test/speaker-test.c:707 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "サスペンドã‹ã‚‰å¾©å¸°å¤±æ•—: %s\n" + +#: speaker-test/speaker-test.c:771 speaker-test/speaker-test.c:1272 +#, c-format +msgid "No enough memory\n" +msgstr "メモリãŒè¶³ã‚Šã¾ã›ã‚“\n" + +#: speaker-test/speaker-test.c:776 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "WAVファイルãŒã‚ªãƒ¼ãƒ—ンã§ãã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:780 speaker-test/speaker-test.c:809 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "䏿­£ãªWAVファイルã§ã™: %s\n" + +#: speaker-test/speaker-test.c:785 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "WAVファイルã§ã¯ã‚りã¾ã›ã‚“: %s\n" + +#: speaker-test/speaker-test.c:789 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "未サãƒãƒ¼ãƒˆã®WAVフォーマット %d: %s\n" + +#: speaker-test/speaker-test.c:794 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s ã¯ãƒ¢ãƒŽã‚¹ãƒˆãƒªãƒ¼ãƒ ã§ã¯ã‚りã¾ã›ã‚“ (%d ãƒãƒ£ãƒãƒ«)\n" + +#: speaker-test/speaker-test.c:799 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "サンプルレートãŒä¸ä¸€è‡´ã§ã™(%d): %s\n" + +#: speaker-test/speaker-test.c:804 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "未サãƒãƒ¼ãƒˆã®ã‚µãƒ³ãƒ—ルフォーマットビット %d: %s\n" + +#: speaker-test/speaker-test.c:865 +#, c-format +msgid "Undefined channel %d\n" +msgstr "未定義ã®ãƒãƒ£ãƒãƒ« %d\n" + +#: speaker-test/speaker-test.c:916 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "書込エラー: %d,%s\n" + +#: speaker-test/speaker-test.c:918 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery 失敗: %d,%s\n" + +#: speaker-test/speaker-test.c:1004 +#, fuzzy, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"使用法: speaker-test [オプション]...\n" +"-h,--help ヘルプ\n" +"-D,--device å†ç”Ÿãƒ‡ãƒã‚¤ã‚¹\n" +"-r,--rate ストリームレート (Hz)\n" +"-c,--channels ãƒãƒ£ãƒãƒ«æ•°\n" +"-f,--frequency 正弦波周波数 (hz)\n" +"-F,--format サンプルフォーマット\n" +"-b,--buffer リングãƒãƒƒãƒ•ァサイズ (μs)\n" +"-p,--period ピリオドサイズ (μs)\n" +"-P,--nperiods ピリオド数\n" +"-t,--test pink=ピンクノイズを使用, sine=正弦波を使用, wav=WAVファ" +"イルå†ç”Ÿ\n" +"-l,--nloops 繰り返ã—テスト数, 0 = 無制é™\n" +"-s,--speaker å˜ä¸€ã‚¹ãƒ”ーカーテスト 1=å·¦ ã¾ãŸã¯ 2=å³\n" +"-w,--wavfile 指定ã®WAVファイルをテスト音æºã¨ã—ã¦ä½¿ç”¨\n" +"-W,--wavdir WAVファイルã®ã‚るディレクトリを指定\n" +"-X,--force-frequency\t30-8000Hz範囲外許å¯\n" +"\n" + +#: speaker-test/speaker-test.c:1136 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "䏿­£ãªãƒ”リオド数 %d\n" + +#: speaker-test/speaker-test.c:1152 speaker-test/speaker-test.c:1156 +#, c-format +msgid "Invalid test type %s\n" +msgstr "䏿­£ãªãƒ†ã‚¹ãƒˆã‚¿ã‚¤ãƒ— %s\n" + +#: speaker-test/speaker-test.c:1189 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "未知ã®ã‚ªãƒ—ション '%c'\n" + +#: speaker-test/speaker-test.c:1203 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "-s オプションã®å€¤ãŒä¸æ­£ã§ã™\n" + +#: speaker-test/speaker-test.c:1218 +#, c-format +msgid "Playback device is %s\n" +msgstr "å†ç”Ÿãƒ‡ãƒã‚¤ã‚¹: %s\n" + +#: speaker-test/speaker-test.c:1219 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "ストリームパラメータ: %iHz, %s, %i ãƒãƒ£ãƒãƒ«\n" + +#: speaker-test/speaker-test.c:1222 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "16 オクターブã®ãƒ”ンクノイズを使用\n" + +#: speaker-test/speaker-test.c:1225 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "正弦波レート: %.4fHz\n" + +#: speaker-test/speaker-test.c:1228 +#, c-format +msgid "WAV file(s)\n" +msgstr "WAV ファイル\n" + +#: speaker-test/speaker-test.c:1238 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "å†ç”Ÿã‚ªãƒ¼ãƒ—ンエラー: %d,%s\n" + +#: speaker-test/speaker-test.c:1244 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "hwparams ã®è¨­å®šã«å¤±æ•—: %s\n" + +#: speaker-test/speaker-test.c:1248 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "swparams ã®è¨­å®šã«å¤±æ•—: %s\n" + +#: speaker-test/speaker-test.c:1295 speaker-test/speaker-test.c:1317 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "転é€ã«å¤±æ•—ã—ã¾ã—ãŸ: %s\n" + +#: speaker-test/speaker-test.c:1303 +#, c-format +msgid "Time per period = %lf\n" +msgstr "ピリオド時間 = %lf\n" + +#~ msgid "Warning: format is changed to S16_LE\n" +#~ msgstr "警告: フォーマット㯠S16_LE ã«å¤‰æ›´ã•れã¾ã™\n" + +#~ msgid "Warning: format is changed to S24_3LE\n" +#~ msgstr "警告: フォーマット㯠S24_3LE ã«å¤‰æ›´ã•れã¾ã™\n" + +#~ msgid "Warning: format is changed to S24_LE\n" +#~ msgstr "警告: フォーマット㯠S24_LE ã«å¤‰æ›´ã•れã¾ã™\n" diff --git a/po/ka.po b/po/ka.po new file mode 100644 index 0000000..4fca706 --- /dev/null +++ b/po/ka.po @@ -0,0 +1,1764 @@ +# alsa-utils translationto Georgian. +# Copyright (C) 2022 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Temuri Doghonadze , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.2.7\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-09-08 09:06+0200\n" +"PO-Revision-Date: 2022-09-09 10:28+0200\n" +"Last-Translator: Temuri Doghonadze \n" +"Language-Team: Georgian <(nothing)>\n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#: alsamixer/card_select.c:77 alsamixer/device_name.c:127 +msgid "Sound Card" +msgstr "ხმის ბáƒáƒ áƒáƒ—ი" + +#: alsamixer/card_select.c:115 +msgid "(default)" +msgstr "(ნáƒáƒ’ულისხმები)" + +#: alsamixer/card_select.c:125 +msgid "cannot enumerate sound cards" +msgstr "ხმის ბáƒáƒ áƒáƒ—ების სიის მიღების შეცდáƒáƒ›áƒ" + +#: alsamixer/card_select.c:151 +msgid "enter device name..." +msgstr "შეიყვáƒáƒœáƒ”თ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის სáƒáƒ®áƒ”ლი..." + +#: alsamixer/cli.c:43 +msgid "Usage: alsamixer [options]" +msgstr "გáƒáƒ›áƒáƒ§áƒ”ნებáƒ: alsamixer [პáƒáƒ áƒáƒ›áƒ”ტრები]" + +#: alsamixer/cli.c:44 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -m, --mouse enable mouse\n" +" -M, --no-mouse disable mouse\n" +" -f, --config=FILE configuration file\n" +" -F, --no-config do not load configuration file\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"სáƒáƒ¡áƒáƒ áƒ’ებლრპáƒáƒ áƒáƒ›áƒ”ტრები:\n" +" -h, --help ეს დáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ\n" +" -c, --card=რიცხვი ხმის ბáƒáƒ áƒáƒ—ის ნáƒáƒ›áƒ”რი áƒáƒœ ID\n" +" -D, --device=სáƒáƒ®áƒ”ლი მიქსერის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის სáƒáƒ®áƒ”ლი\n" +" -m, --mouse თáƒáƒ’უნáƒáƒ¡ ჩáƒáƒ áƒ—ვáƒ\n" +" -M, --no-mouse თáƒáƒ’უნáƒáƒ¡ გáƒáƒ›áƒáƒ áƒ—ვáƒ\n" +" -f, --config=ფáƒáƒ˜áƒšáƒ˜ კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ ფáƒáƒ˜áƒšáƒ˜\n" +" -F, --no-config კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ ფáƒáƒ˜áƒšáƒ˜ áƒáƒ  ჩáƒáƒ˜áƒ¢áƒ•ირთებáƒ\n" +" -V, --view=რეჟიმი ხედის რეჟიმის გáƒáƒ¨áƒ•ებáƒ: playback (დáƒáƒ™áƒ•რáƒ)/record " +"(ჩáƒáƒ­áƒ”რáƒ)/all (ყველáƒ)" + +#: alsamixer/cli.c:53 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"გáƒáƒ›áƒáƒ áƒ—ვის პáƒáƒ áƒáƒ›áƒ”ტრები:\n" +" -g, --no-color ფერების გáƒáƒ›áƒáƒ§áƒ”ნების გáƒáƒ“áƒáƒ áƒ—ვáƒ\n" +" -a, --abstraction=სáƒáƒ®áƒ”ლი მიქსერის áƒáƒ‘სტრáƒáƒ¥áƒªáƒ˜áƒ˜áƒ¡ დáƒáƒœáƒ”: none(áƒáƒ áƒª ერთი)/" +"basic (ძირითáƒáƒ“ი)" + +#: alsamixer/cli.c:88 +#, c-format +msgid "invalid card index: %s\n" +msgstr "ბáƒáƒ áƒáƒ—ის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ინდექსი: %s\n" + +#: alsamixer/cli.c:130 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "უცნáƒáƒ‘ი áƒáƒ‘სტრáƒáƒ¥áƒªáƒ˜áƒ˜áƒ¡ დáƒáƒœáƒ”: %s\n" + +#: alsamixer/cli.c:135 +#, c-format +msgid "unknown option: %c\n" +msgstr "უცნáƒáƒ‘ი პáƒáƒ áƒáƒ›áƒ”ტრი: %c\n" + +#: alsamixer/cli.c:137 +msgid "try `alsamixer --help' for more information\n" +msgstr "მეტი ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡áƒ—ვის სცáƒáƒ“ეთ 'alsamixer --help'\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის სáƒáƒ®áƒ”ლი:" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "%s: %s\n" + +#: alsamixer/mixer_display.c:99 +msgid "Card:" +msgstr "ბáƒáƒ áƒáƒ—ი:" + +#: alsamixer/mixer_display.c:100 +msgid "Chip:" +msgstr "ჩიპი:" + +#: alsamixer/mixer_display.c:101 +msgid "View:" +msgstr "ხედი:" + +#: alsamixer/mixer_display.c:102 +msgid "Item:" +msgstr "ელემენტი:" + +#: alsamixer/mixer_display.c:105 +msgid "F1: Help" +msgstr "F1: დáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ" + +#: alsamixer/mixer_display.c:106 +msgid "F2: System information" +msgstr "F2: ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ სისტემის შესáƒáƒ®áƒ”ბ" + +#: alsamixer/mixer_display.c:107 +msgid "F6: Select sound card" +msgstr "F6: áƒáƒ˜áƒ áƒ©áƒ”თ ხმის ბáƒáƒ áƒáƒ—ი" + +#: alsamixer/mixer_display.c:108 +msgid "Esc: Exit" +msgstr "Esc: გáƒáƒ¡áƒ•ლáƒ" + +#: alsamixer/mixer_display.c:179 +msgid "(unplugged)" +msgstr "(გáƒáƒ›áƒáƒ”რთებული)" + +#: alsamixer/mixer_display.c:197 +msgid "Playback" +msgstr "დáƒáƒ™áƒ•რáƒ" + +#: alsamixer/mixer_display.c:198 +msgid "Capture" +msgstr "ჩáƒáƒ­áƒ”რáƒ" + +#: alsamixer/mixer_display.c:199 +msgid "All" +msgstr "&ყველáƒ" + +#: alsamixer/mixer_display.c:240 +msgid "mute" +msgstr "დáƒáƒ“უმებáƒ" + +#: alsamixer/mixer_display.c:281 alsamixer/mixer_display.c:291 +msgid "dB gain:" +msgstr "გáƒáƒ«áƒšáƒ˜áƒ”რებრ(დბ):" + +#: alsamixer/mixer_display.c:291 +#, c-format +msgid " [%s %s, %s]" +msgstr " [%s %s, %s]" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:306 +#: alsamixer/mixer_display.c:312 alsamixer/mixer_display.c:318 +msgid "Off" +msgstr "გáƒáƒ›áƒáƒ áƒ—ული" + +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:318 +msgid "On" +msgstr "ჩáƒáƒ áƒ—ული" + +#: alsamixer/mixer_display.c:370 +msgid "The sound device was unplugged." +msgstr "ხმის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რგáƒáƒ›áƒáƒ”რთებულიáƒ." + +#: alsamixer/mixer_display.c:371 +msgid "Press F6 to select another sound card." +msgstr "სხვრხმის ბáƒáƒ áƒáƒ—ის áƒáƒ¡áƒáƒ áƒ©áƒ”ვáƒáƒ“ დáƒáƒáƒ¬áƒ”ქით F6-ს." + +#: alsamixer/mixer_display.c:386 +msgid "This sound device does not have any playback controls." +msgstr "áƒáƒ› ხმის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ¡ დáƒáƒ™áƒ•რის áƒáƒ áƒáƒœáƒáƒ˜áƒ áƒ˜ კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜ áƒáƒ  გáƒáƒáƒ©áƒœáƒ˜áƒ." + +#: alsamixer/mixer_display.c:388 +msgid "This sound device does not have any capture controls." +msgstr "áƒáƒ› ხმის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ¡ ჩáƒáƒ­áƒ”რის áƒáƒ áƒáƒœáƒáƒ˜áƒ áƒ˜ კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜ áƒáƒ  გáƒáƒáƒ©áƒœáƒ˜áƒ." + +#: alsamixer/mixer_display.c:390 +msgid "This sound device does not have any controls." +msgstr "áƒáƒ› ხმის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ¡ áƒáƒ áƒáƒœáƒáƒ˜áƒ áƒ˜ კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜ áƒáƒ  გáƒáƒáƒ©áƒœáƒ˜áƒ." + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:523 alsamixer/mixer_display.c:528 +msgid "O" +msgstr "O" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:525 alsamixer/mixer_display.c:529 +msgid "M" +msgstr "მ" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:545 +msgid "L" +msgstr "მხ" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:551 +msgid "R" +msgstr "მჯ" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:555 +msgid "CAPTURE" +msgstr "ჩáƒáƒ­áƒ”რáƒ" + +#: alsamixer/mixer_display.c:611 +msgid "Front" +msgstr "წინáƒ" + +#: alsamixer/mixer_display.c:614 +msgid "Rear" +msgstr "უკáƒáƒœáƒ" + +#: alsamixer/mixer_display.c:617 speaker-test/speaker-test.c:124 +msgid "Center" +msgstr "ცენტრი" + +#: alsamixer/mixer_display.c:620 +msgid "Woofer" +msgstr "ვუფერი" + +#: alsamixer/mixer_display.c:623 +msgid "Side" +msgstr "გვერდითი" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "მიქსერის გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "მიქსერის კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜áƒ¡ ჩáƒáƒ¢áƒ•ირთვის შეცდáƒáƒ›áƒ" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "მიქსერის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ: '%s'." + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc გáƒáƒ¡áƒ•ლáƒ" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H დáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ სისტემის შესáƒáƒ®áƒ”ბ" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 დáƒáƒ™áƒ•რის კáƒáƒœáƒ¢áƒ áƒáƒšáƒ”ბის ჩვენებáƒ" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 ჩáƒáƒ­áƒ”რის კáƒáƒœáƒ¢áƒ áƒáƒšáƒ”ბის ჩვენებáƒ" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 ყველრკáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜áƒ¡ ჩვენებáƒ" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab ხედის რეჟიმს გáƒáƒ“áƒáƒ áƒ—áƒáƒ•ს (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F6 S áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ხმის ბáƒáƒ áƒáƒ—ი" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L ეკრáƒáƒœáƒ˜áƒ¡ გáƒáƒ“áƒáƒ®áƒáƒ¢áƒ•áƒ" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "მáƒáƒ áƒªáƒ®áƒœáƒ˜áƒ• წინრკáƒáƒœáƒ¢áƒ áƒáƒšáƒ–ე გáƒáƒ“áƒáƒ¡áƒ•ლáƒ" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "მáƒáƒ áƒ¯áƒ•ნივ შემდეგ კáƒáƒœáƒ¢áƒ áƒáƒšáƒ–ე გáƒáƒ“áƒáƒ¡áƒ•ლáƒ" + +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "მáƒáƒ¦áƒšáƒ/დáƒáƒ‘ლრხმის შეცვლáƒ" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - ხმის შეცვლáƒ" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "Page Up/Dn ხმის დიდი ნáƒáƒ‘იჯებით შეცვლáƒ" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "End ხმის 0%-ზე დáƒáƒ§áƒ”ნებáƒ" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 ხმის დáƒáƒ§áƒ”ნებრto 0%-90%-ზე" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E მáƒáƒ áƒªáƒ®áƒ”ნáƒ/áƒáƒ áƒ˜áƒ•ე/მáƒáƒ áƒ¯áƒ•ენრáƒáƒ áƒ®áƒ˜áƒ¡ ხმის გáƒáƒ–რდáƒ" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Z X C მáƒáƒ áƒªáƒ®áƒ”ნáƒ/áƒáƒ áƒ˜áƒ•ე/მáƒáƒ áƒ¯áƒ•ენრáƒáƒ áƒ®áƒ˜áƒ¡ ხმის შემცირებáƒ" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B მáƒáƒ áƒ¯áƒ•ენრდრმáƒáƒ áƒªáƒ®áƒ”ნრáƒáƒ áƒ®áƒ”ბის დáƒáƒ‘áƒáƒšáƒáƒœáƒ¡áƒ”ბáƒ" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M დáƒáƒ“უმების გáƒáƒ“áƒáƒ áƒ—ვáƒ" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr "< > მáƒáƒ áƒªáƒ®áƒ”ნáƒ/მáƒáƒ áƒ¯áƒ•ენრáƒáƒ áƒ®áƒ”ბის დáƒáƒ“უმებáƒ" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "Space ჩáƒáƒ­áƒ”რის გáƒáƒ“áƒáƒ áƒ—ვáƒ" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "; ' მáƒáƒ áƒªáƒ®áƒ”ნáƒ/მáƒáƒ áƒ¯áƒ•ენრáƒáƒ áƒ®áƒ˜áƒ¡ ჩáƒáƒ­áƒ”რის გáƒáƒ“áƒáƒ áƒ—ვáƒ" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "áƒáƒ•ტáƒáƒ áƒ”ბი:" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr " Tim Janik" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr " Jaroslav Kysela " + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr " Clemens Ladisch " + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "დáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ" + +#: alsamixer/proc_files.c:56 +msgid "Select File" +msgstr "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ფáƒáƒ˜áƒšáƒ˜" + +#: alsamixer/textbox.c:51 alsamixer/textbox.c:65 +msgid "Error" +msgstr "შეცდáƒáƒ›áƒ" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ: %s." + +#: aplay/aplay.c:180 +msgid "raw data" +msgstr "დáƒáƒ£áƒ›áƒ£áƒ¨áƒáƒ•ებელი მáƒáƒœáƒáƒªáƒ”მი" + +#: aplay/aplay.c:181 +msgid "VOC" +msgstr "VOC" + +#: aplay/aplay.c:183 +msgid "WAVE" +msgstr "WAVE" + +#: aplay/aplay.c:184 +msgid "Sparc Audio" +msgstr "Sparc áƒáƒ£áƒ“იáƒ" + +#: aplay/aplay.c:205 +#, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file " +"name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"გáƒáƒ›áƒáƒ§áƒ”ნებáƒ: %s [პáƒáƒ áƒáƒ›áƒ”ტრი]... [ფáƒáƒ˜áƒšáƒ˜]...\n" +"\n" +"-h, --help დáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ\n" +" --version მიმდინáƒáƒ áƒ” ვერსიის გáƒáƒ›áƒáƒ¢áƒáƒœáƒ\n" +"-l, --list-devices ხმის ბáƒáƒ áƒáƒ—ებისრდრციფრული áƒáƒ£áƒ“ირმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების " +"სიáƒ\n" +"-L, --list-pcms მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების სáƒáƒ®áƒ”ლების სიáƒ\n" +"-D, --device=NAME PCM -ის სáƒáƒ®áƒ”ლით áƒáƒ áƒ©áƒ”ვáƒ\n" +"-q, --quiet ჩუმი რეჟიმი\n" +"-t, --file-type TYPE ფáƒáƒ˜áƒšáƒ˜áƒ¡ ტიპი (voc, wav, raw áƒáƒœ au)\n" +"-c, --channels=# áƒáƒ áƒ®áƒ”ბი\n" +"-f, --format=FORMAT სემპლის ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ (მგრძნáƒáƒ‘იáƒáƒ áƒ”რრეგისტრზე)\n" +"-r, --rate=# სემპლის სიხშირე\n" +"-d, --duration=# შეწყვეტრ# წáƒáƒ›áƒ˜áƒ¡ შემდეგ\n" +"-s, --samples=# შეწყვეტრ# სემპლის შემდეგ თითáƒáƒ”ულ áƒáƒ áƒ®áƒ–ე\n" +"-M, --mmap mmap -ის ნáƒáƒ™áƒáƒ“ი\n" +"-N, --nonblock áƒáƒ áƒáƒ‘ლáƒáƒ™áƒ˜áƒ áƒ”ბáƒáƒ“ი რეჟიმი\n" +"-F, --period-time=# წყვეტებს შáƒáƒ áƒ˜áƒ¡ დáƒáƒ¨áƒáƒ áƒ”ბრ# მიკრáƒáƒ¬áƒáƒ›áƒ˜áƒ\n" +"-B, --buffer-time=# ბáƒáƒ¤áƒ”რის სიგრძე # მიკრáƒáƒ¬áƒáƒ›áƒ˜áƒ\n" +" --period-size=# წყვეტების სიგრძე # კáƒáƒ“რიáƒ\n" +" --buffer-size=# ბáƒáƒ¤áƒ”რის სიგრძე # კáƒáƒ“რიáƒ\n" +"-A, --avail-min=# მინიმáƒáƒšáƒ£áƒ áƒ˜ ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜ სივრცე გáƒáƒ¦áƒ•იძებისთვის # " +"მიკრáƒáƒ¬áƒáƒ›áƒ˜áƒ\n" +"-R, --start-delay=# PCM -ის გáƒáƒ¨áƒ•ების áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ დáƒáƒ§áƒáƒ•ნებრ# მიკრáƒáƒ¬áƒáƒ›áƒ˜áƒ\n" +" (ბáƒáƒ¤áƒ”რის ზáƒáƒ›áƒáƒ¡áƒ—áƒáƒœ შედáƒáƒ áƒ”ბით, თუ ის <= 0)\n" +"-T, --stop-delay=# PCM-ის áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ გáƒáƒ©áƒ”რების დáƒáƒ§áƒáƒ•ნებრxrun-დáƒáƒœ # " +"მიკრáƒáƒ¬áƒáƒ›áƒ˜áƒ\n" +"-v, --verbose PCM-ის სტრუქტურრდრმáƒáƒ áƒ’ებრ(დáƒáƒ’რáƒáƒ•ებáƒáƒ“ი)\n" +"-V, --vumeter=TYPE VU-ის მზáƒáƒ›áƒáƒ•ის ჩáƒáƒ áƒ—ვრ(ტიპი: mono áƒáƒœ stereo)\n" +"-I, --separate-channels ერთი ფáƒáƒ˜áƒšáƒ˜ ერთი áƒáƒ áƒ®áƒ˜áƒ¡áƒ—ვის\n" +"-i, --interactive ინტერáƒáƒ¥áƒ¢áƒ˜áƒ£áƒ áƒ˜ ურთიერთáƒáƒ‘რstdin-დáƒáƒœ\n" +"-m, --chmap=ch1,ch2,.. áƒáƒ áƒ®áƒ”ბის რუკრგáƒáƒ“áƒáƒ¡áƒáƒ¤áƒáƒ áƒáƒ“ áƒáƒœ მისáƒáƒ§áƒáƒšáƒáƒ“\n" +" --disable-resample სიხშირის áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ რესემპლის გáƒáƒ›áƒáƒ áƒ—ვáƒ\n" +" --disable-channels áƒáƒ áƒ®áƒ”ბის áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ გáƒáƒ áƒ“áƒáƒ¥áƒ›áƒœáƒ˜áƒ¡ გáƒáƒ›áƒáƒ áƒ—ვáƒ\n" +" --disable-format ფáƒáƒ áƒ›áƒáƒ¢áƒ˜áƒ¡ áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ გáƒáƒ áƒ“áƒáƒ¥áƒ›áƒœáƒ˜áƒ¡ გáƒáƒ›áƒáƒ áƒ—ვáƒ\n" +" --disable-softvol ხმის პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ áƒ—ვრ(softvol)\n" +" --test-position ბეჭდის ბáƒáƒ¤áƒ”რის მდებáƒáƒ áƒ”áƒáƒ‘ის ტესტი\n" +" --test-coef=# ბეჭდის ბáƒáƒ¤áƒ”რის მდებáƒáƒ áƒ”áƒáƒ‘ის სáƒáƒ¢áƒ”სტრკáƒáƒ”ფიციენტი " +"(ნáƒáƒ’ულისხმები 8)\n" +" შემáƒáƒ¬áƒ›áƒ”ბის გáƒáƒ›áƒáƒ¡áƒáƒ®áƒ£áƒšáƒ”ბáƒáƒ: coef * (buffer_size / 2)\n" +" --test-nowait áƒáƒ  დáƒáƒ£áƒªáƒ“ის ბეჭდის ბáƒáƒ¤áƒ”რს (სრულáƒáƒ“ ხáƒáƒ áƒ¯áƒáƒ•ს CPU-ს " +"რესურსს)\n" +" --max-file-time=# რáƒáƒªáƒ ძველი ფáƒáƒ˜áƒšáƒ˜ მითითებული რáƒáƒáƒ“ენáƒáƒ‘áƒáƒ¯áƒ”რ " +"ჩáƒáƒ˜áƒ¬áƒ”რებáƒ, \n" +"\t\t\t\t\t\táƒáƒ®áƒáƒšáƒ˜ ფáƒáƒ˜áƒšáƒ˜áƒ¡ დáƒáƒ¬áƒ§áƒ”ბáƒ\n" +" --process-id-file პრáƒáƒªáƒ”სის ID-ის ფáƒáƒ˜áƒšáƒ¨áƒ˜ ჩáƒáƒ¬áƒ”რáƒ\n" +" --use-strftime გáƒáƒ›áƒáƒ¢áƒáƒœáƒ˜áƒ¡ ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლზე strftime-ის გáƒáƒ“áƒáƒ¢áƒáƒ áƒ”ბáƒ\n" +" --dump-hw-params მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების ფáƒáƒ˜áƒšáƒ¨áƒ˜ " +"ჩáƒáƒ¬áƒ”რáƒ\n" +" --fatal-errors ყველრშეცდáƒáƒ›áƒ˜áƒ¡ ფáƒáƒ¢áƒáƒšáƒ£áƒ áƒáƒ“ ჩáƒáƒ—ვლáƒ\n" + +#: aplay/aplay.c:250 speaker-test/speaker-test.c:961 +#, c-format +msgid "Recognized sample formats are:" +msgstr "სემპლის მისáƒáƒ¦áƒ”ბი ფáƒáƒ áƒ›áƒáƒ¢áƒ”ბიáƒ:" + +#: aplay/aplay.c:256 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"áƒáƒ¥áƒ”დáƒáƒœ ზáƒáƒ’იერთი შეიძლებრáƒáƒ áƒ©áƒ”ულ áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒáƒ–ე წვდáƒáƒ›áƒáƒ“ი áƒáƒ  იყáƒáƒ¡\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "The available format shortcuts are:\n" +msgstr "ფáƒáƒ áƒ›áƒáƒ¢áƒ”ბის ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜ შემáƒáƒ™áƒšáƒ”ბები:\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16 bit little endian, 44100, stereo)\n" + +#: aplay/aplay.c:259 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16 bit big endian, 44100, stereo)\n" + +#: aplay/aplay.c:260 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16 bit little endian, 48000, stereo)\n" + +#: aplay/aplay.c:274 +msgid "no soundcards found..." +msgstr "ხმის ბáƒáƒ áƒáƒ—ის პáƒáƒ•ნრშეუძლებელიáƒ..." + +#: aplay/aplay.c:277 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** %s áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის სირ****\n" + +#: aplay/aplay.c:306 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "ბáƒáƒ áƒáƒ—ი %i: %s [%s], მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რ%i: %s [%s]\n" + +#: aplay/aplay.c:312 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " ქვემáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ები: %i/%i\n" + +#: aplay/aplay.c:319 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " ქვემáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რ#%i: %s\n" + +#: aplay/aplay.c:400 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "áƒáƒ•áƒáƒ áƒ˜áƒ£áƒšáƒáƒ“ დáƒáƒ¡áƒ áƒ£áƒšáƒ“რსიგნáƒáƒšáƒ˜áƒ— %s...\n" + +#: aplay/aplay.c:555 +msgid "command should be named either arecord or aplay" +msgstr "ბრძáƒáƒœáƒ”ბáƒáƒ¡ arecord áƒáƒœ aplay უნდრერქვáƒáƒ¡" + +#: aplay/aplay.c:599 +#, c-format +msgid "unrecognized file format %s" +msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ უცნáƒáƒ‘ი ფáƒáƒ áƒ›áƒáƒ¢áƒ˜: %s" + +#: aplay/aplay.c:606 +#, c-format +msgid "invalid channels argument '%s'" +msgstr "áƒáƒ áƒ®áƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:610 +#, c-format +msgid "value %i for channels is invalid" +msgstr "áƒáƒ áƒ®áƒ”ბისთვის მნიშვნელáƒáƒ‘რ\"%i\" áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜áƒ" + +#: aplay/aplay.c:630 +#, c-format +msgid "wrong extended format '%s'" +msgstr "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ გáƒáƒ¤áƒáƒ áƒ—áƒáƒ”ბული ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ \"%s\"" + +#: aplay/aplay.c:638 +#, c-format +msgid "invalid rate argument '%s'" +msgstr "სიხშირის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:645 +#, c-format +msgid "bad speed value %i" +msgstr "სიჩქáƒáƒ áƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ მნიშვნელáƒáƒ‘áƒ: %i" + +#: aplay/aplay.c:651 +msgid "duration and samples arguments cannot be used together" +msgstr "ხáƒáƒœáƒ’რძლივáƒáƒ‘ის დრსემპლის áƒáƒ áƒ’უმენტებს ერთდრულáƒáƒ“ ვერ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებთ" + +#: aplay/aplay.c:656 +#, c-format +msgid "invalid duration argument '%s'" +msgstr "ხáƒáƒœáƒ’რძლივáƒáƒ‘ს áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:663 +msgid "samples and duration arguments cannot be used together" +msgstr "ხáƒáƒœáƒ’რძლივáƒáƒ‘ის დრსემპლის áƒáƒ áƒ’უმენტებს ერთდრულáƒáƒ“ ვერ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებთ" + +#: aplay/aplay.c:668 +#, c-format +msgid "invalid samples argument '%s'" +msgstr "სემპლის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:680 +#, c-format +msgid "invalid period time argument '%s'" +msgstr "დრáƒáƒ˜áƒ¡ პერიáƒáƒ“ის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:687 +#, c-format +msgid "invalid buffer time argument '%s'" +msgstr "ბუფერის დრáƒáƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:694 +#, c-format +msgid "invalid period size argument '%s'" +msgstr "პერიáƒáƒ“ის ზáƒáƒ›áƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:701 +#, c-format +msgid "invalid buffer size argument '%s'" +msgstr "ბუფერის ზáƒáƒ›áƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:708 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "მინინáƒáƒšáƒ£áƒ áƒ˜ ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜ áƒáƒ“გილის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:715 +#, c-format +msgid "invalid start delay argument '%s'" +msgstr "დáƒáƒ¬áƒ§áƒ”ბის დáƒáƒ§áƒáƒ•ნების áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:722 +#, c-format +msgid "invalid stop delay argument '%s'" +msgstr "გáƒáƒ©áƒ”რების დáƒáƒ§áƒáƒ•ნების áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:777 +#, c-format +msgid "invalid test coef argument '%s'" +msgstr "სáƒáƒ¢áƒ”სტრკáƒáƒ”ფიც. áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი: '%s'" + +#: aplay/aplay.c:789 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "მáƒáƒ¥áƒ¡áƒ˜áƒ›áƒáƒšáƒ£áƒ áƒ˜ ფáƒáƒ˜áƒšáƒ˜áƒ¡ დრáƒáƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტი:'%s'" + +#: aplay/aplay.c:809 speaker-test/speaker-test.c:587 +#, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "áƒáƒ áƒ®áƒ”ბის რუკის სტრიქáƒáƒœáƒ˜áƒ¡ დáƒáƒ›áƒ£áƒ¨áƒáƒ•ების შეცდáƒáƒ›áƒ: %s\n" + +#: aplay/aplay.c:815 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "მეტი ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡áƒ—ვის სცáƒáƒ“ეთ '%s --help'\n" + +#: aplay/aplay.c:831 +#, c-format +msgid "audio open error: %s" +msgstr "áƒáƒ£áƒ“იáƒáƒ¡ გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ პრáƒáƒ‘ლემáƒ: %s" + +#: aplay/aplay.c:836 +#, c-format +msgid "info error: %s" +msgstr "ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡ შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:843 +#, c-format +msgid "nonblock setting error: %s" +msgstr "áƒáƒ áƒáƒ‘ლáƒáƒ™áƒ˜áƒ áƒ”ბáƒáƒ“ი პáƒáƒ áƒáƒ›áƒ”ტრის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:861 aplay/aplay.c:985 aplay/aplay.c:1275 aplay/aplay.c:1475 +#: aplay/aplay.c:2055 aplay/aplay.c:2085 +msgid "not enough memory" +msgstr "მეხსიერებრსáƒáƒ™áƒ›áƒáƒ áƒ˜áƒ¡áƒ˜ áƒáƒ  áƒáƒ áƒ˜áƒ¡" + +#: aplay/aplay.c:885 +#, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "პრáƒáƒªáƒ”სის ID-ის ფáƒáƒ˜áƒšáƒ˜áƒ¡ (\"%s\") შექმნის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:975 +#, c-format +msgid "read error (called from line %i)" +msgstr "კითხვის შეცდáƒáƒ›áƒ (გáƒáƒ›áƒáƒ«áƒáƒ®áƒ”ბულირხáƒáƒ–იდáƒáƒœ %i)" + +#: aplay/aplay.c:1042 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "'fmt'-ის ნáƒáƒ¬áƒ˜áƒšáƒ˜áƒ¡ უცნáƒáƒ‘ი სიგრძე (წáƒáƒ•იკითხე %u, %u მáƒáƒ˜áƒœáƒª უნდრიყáƒáƒ¡)" + +#: aplay/aplay.c:1053 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"გáƒáƒ’რძელებáƒáƒ“ი 'fmt' ნáƒáƒ¬áƒ˜áƒšáƒ˜áƒ¡ უცნáƒáƒ‘ი სიგრძე (წáƒáƒ•იკითხე %u, %u მáƒáƒ˜áƒœáƒª უნდრიყáƒáƒ¡)" + +#: aplay/aplay.c:1058 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "ფáƒáƒ áƒ›áƒáƒ¢áƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ჭდე გáƒáƒ¤áƒáƒ áƒ—áƒáƒ”ბáƒáƒ“ 'fmt' ნáƒáƒ¬áƒ˜áƒšáƒ¨áƒ˜" + +#: aplay/aplay.c:1065 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "" +"შეუძლებელირWAVE ფáƒáƒ˜áƒšáƒ˜áƒ¡ დáƒáƒ™áƒ•რრ(ფáƒáƒ áƒ›áƒáƒ¢áƒ˜áƒ— 0x%04x), რáƒáƒ›áƒ”ლიც PCM-ის áƒáƒœ FLOAT-" +"ით áƒáƒ áƒáƒ კáƒáƒ“ირებული" + +#: aplay/aplay.c:1070 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "შეუძლებელირWAVE ფáƒáƒ˜áƒšáƒ˜áƒ¡ დáƒáƒ™áƒ•რრ%d áƒáƒ£áƒ“იáƒáƒ‘ილიკით" + +#: aplay/aplay.c:1078 aplay/aplay.c:1204 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ: ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ U8-ზე შეიცვáƒáƒšáƒ\n" + +#: aplay/aplay.c:1088 aplay/aplay.c:1101 aplay/aplay.c:1112 +#, c-format +msgid "Warning: format is changed to %s\n" +msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ: ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ %s-ზე შეიცვáƒáƒšáƒ\n" + +#: aplay/aplay.c:1117 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr "" +" შეუძლებელირWAVE ფáƒáƒ˜áƒšáƒ˜áƒ¡ დáƒáƒ™áƒ•რრსემპლით %d ბიტი, %d ბáƒáƒ˜áƒ¢áƒ˜áƒ¡ სიგáƒáƒœáƒ”ში (%d " +"áƒáƒ áƒ®áƒ˜)" + +#: aplay/aplay.c:1140 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr " შეუძლებელირWAVE ფáƒáƒ˜áƒšáƒ˜áƒ¡ დáƒáƒ™áƒ•რáƒ, %d ბიტიáƒáƒœáƒ˜ სიგáƒáƒœáƒ˜áƒ¡ მქáƒáƒœáƒ” სემპლით" + +#: aplay/aplay.c:1198 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ: ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ MU_LAW-ზე შეიცვáƒáƒšáƒ\n" + +#: aplay/aplay.c:1210 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ: ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ S16_BE-ზე შეიცვáƒáƒšáƒ\n" + +#: aplay/aplay.c:1223 aplay/aplay.c:2376 aplay/aplay.c:2383 aplay/aplay.c:2912 +msgid "read error" +msgstr "წáƒáƒ™áƒ˜áƒ—ხვის შეცდáƒáƒ›áƒ" + +#: aplay/aplay.c:1253 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "áƒáƒ áƒ®áƒ”ბის რáƒáƒáƒ“ენáƒáƒ‘რhw_params დრchannel_map-ში ერმáƒáƒœáƒ”თს áƒáƒ  ემთხვევáƒ" + +#: aplay/aplay.c:1262 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ: áƒáƒ áƒ®áƒ”ბის რუკის მიღების შეცდáƒáƒ›áƒ\n" + +#: aplay/aplay.c:1296 +#, c-format +msgid "Channel %d doesn't match with hw_params" +msgstr "áƒáƒ áƒ®áƒ˜ %d hw_params-ში áƒáƒ áƒ¡áƒ”ბულს áƒáƒ  ემთხვევáƒ" + +#: aplay/aplay.c:1323 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "დáƒáƒ–იáƒáƒœáƒ”ბული კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ áƒáƒ› PCM-ისთვის: კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ მიუწვდáƒáƒ›áƒ”ლიáƒ" + +#: aplay/aplay.c:1327 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "HW პáƒáƒ áƒáƒ›áƒ”ტრები მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ისთვის \"%s\".\n" + +#: aplay/aplay.c:1347 +msgid "Access type not available" +msgstr "წვდáƒáƒ›áƒ˜áƒ¡ ტიპი ხელმიუწვდáƒáƒ›áƒ”ლიáƒ" + +#: aplay/aplay.c:1352 +msgid "Sample format non available" +msgstr "სემპლის ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ ხელიმიუწვდáƒáƒ›áƒ”ლიáƒ" + +#: aplay/aplay.c:1358 +msgid "Channels count non available" +msgstr "áƒáƒ áƒ®áƒ”ბის რáƒáƒáƒ“ენáƒáƒ‘რხელიუწვდáƒáƒ›áƒ”ლიáƒ" + +#: aplay/aplay.c:1373 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "გáƒáƒ¤áƒ áƒ—ხლებáƒ: სიხშირე სწáƒáƒ áƒ˜ áƒáƒ áƒáƒ (მáƒáƒ—ხáƒáƒ•ნილი = %iჰც, მივიღე = %iჰც)\n" + +#: aplay/aplay.c:1379 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " სცáƒáƒ“ეთ შეერთáƒáƒ— დáƒáƒ›áƒáƒ¢áƒ”ბრ%s\n" + +#: aplay/aplay.c:1416 +msgid "Unable to install hw params:" +msgstr "áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ:" + +#: aplay/aplay.c:1423 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "ბუფერის ზáƒáƒ›áƒ˜áƒ¡ ტáƒáƒš პერიáƒáƒ“ს ვერ გáƒáƒ›áƒáƒ•იყენებ (%lu == %lu)" + +#: aplay/aplay.c:1429 +msgid "Unable to get current sw params." +msgstr "პრáƒáƒ’რáƒáƒ›áƒ˜áƒ¡ მიმდინáƒáƒ áƒ” პáƒáƒ áƒáƒ›áƒ”ტრების მიღების შეცდáƒáƒ›áƒ." + +#: aplay/aplay.c:1458 +msgid "unable to install sw params:" +msgstr "პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ:" + +#: aplay/aplay.c:1493 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "snd_pcm_mmap_begin -ის პრáƒáƒ‘ლემáƒ: %s" + +#: aplay/aplay.c:1522 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "stdin-დáƒáƒœ O_NONBLOCK áƒáƒšáƒ›áƒ˜áƒ¡ დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ\n" + +#: aplay/aplay.c:1559 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "" +"\rშეჩერების ბრძáƒáƒœáƒ”ბრიგნáƒáƒ áƒ˜áƒ áƒ”ბულირ(áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ მხáƒáƒ áƒ“áƒáƒ­áƒ”რრáƒáƒ  áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს)\n" + +#: aplay/aplay.c:1567 +#, c-format +msgid "pause push error: %s" +msgstr "შეჩერების გáƒáƒ“áƒáƒªáƒ”მის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:1578 +#, c-format +msgid "pause release error: %s" +msgstr "შეჩერების მáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:1594 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" +"\r=== შეჩერებრ" +"=== " + +#: aplay/aplay.c:1636 +#, c-format +msgid "status error: %s" +msgstr "შეცდáƒáƒ›áƒ˜áƒ¡ სტáƒáƒ¢áƒ£áƒ¡áƒ˜: %s" + +#: aplay/aplay.c:1641 +#, c-format +msgid "fatal %s: %s" +msgstr "გáƒáƒ¢áƒáƒšáƒ£áƒ áƒ˜ %s: %s" + +#: aplay/aplay.c:1642 aplay/aplay.c:1653 aplay/aplay.c:1656 aplay/aplay.c:1664 +msgid "underrun" +msgstr "áƒáƒ áƒ¨áƒ”ვსებáƒ" + +#: aplay/aplay.c:1642 aplay/aplay.c:1653 aplay/aplay.c:1664 +msgid "overrun" +msgstr "გáƒáƒ“áƒáƒ•სებáƒ" + +#: aplay/aplay.c:1652 aplay/aplay.c:1663 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (სულ ცáƒáƒ¢áƒ %.3f მწმ სიგრძით)\n" + +#: aplay/aplay.c:1668 +#, c-format +msgid "Status:\n" +msgstr "სტáƒáƒ¢áƒ£áƒ¡áƒ˜:\n" + +#: aplay/aplay.c:1672 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "xrun: მáƒáƒ›áƒ–áƒáƒ“ების შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:1679 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "სტáƒáƒ¢áƒ£áƒ¡áƒ˜(DRAINING):\n" + +#: aplay/aplay.c:1683 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "ჩáƒáƒ¡áƒáƒ­áƒ”რი ნáƒáƒ™áƒáƒ“ის ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ შეიცვáƒáƒšáƒ? ვცდილáƒáƒ‘ áƒáƒ¦áƒ•დგე....\n" + +#: aplay/aplay.c:1685 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "xrun(DRAINING): მáƒáƒ›áƒ–áƒáƒ“ების შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:1692 +#, c-format +msgid "Status(R/W):\n" +msgstr "სტáƒáƒ¢áƒ£áƒ¡áƒ˜(R/W):\n" + +#: aplay/aplay.c:1695 +#, c-format +msgid "read/write error, state = %s" +msgstr "წáƒáƒ™áƒ˜áƒ—ხვáƒ/ჩáƒáƒ¬áƒ”რის შეცდáƒáƒ›áƒ. მდგáƒáƒ›áƒáƒ áƒ”áƒáƒ‘რ= %s" + +#: aplay/aplay.c:1705 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "შეჩერებულიáƒ. ვცდილáƒáƒ‘, გáƒáƒ•áƒáƒ’რძელáƒ. " + +#: aplay/aplay.c:1711 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "შეცდáƒáƒ›áƒ. ნáƒáƒ™áƒáƒ“ის გáƒáƒ“áƒáƒ¢áƒ•ირთვáƒ. " + +#: aplay/aplay.c:1714 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "შეჩერებáƒ: მáƒáƒ›áƒ–áƒáƒ“ების შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:1719 +#, c-format +msgid "Done.\n" +msgstr "დáƒáƒ¡áƒ áƒ£áƒšáƒ“áƒ.\n" + +#: aplay/aplay.c:1741 +#, c-format +msgid " !clip " +msgstr " !clip " + +#: aplay/aplay.c:1893 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "ბიტის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ზáƒáƒ›áƒ %d.\n" + +#: aplay/aplay.c:1929 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "მáƒáƒ¥áƒ¡ პიკი (%li სემპლი): 0x%08x " + +#: aplay/aplay.c:1970 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "" +"ბუფერის სáƒáƒ”ჭვრმდებáƒáƒ áƒ”áƒáƒ‘რ(%li სულ): ხელმის. = %li, დáƒáƒ§áƒáƒ•ნებრ= %li, ბუფერი " +"= %li\n" + +#: aplay/aplay.c:1979 +#, c-format +msgid "" +"Suspicious status buffer position (%li total): avail = %li, delay = %li, " +"buffer = %li\n" +msgstr "" +"სტáƒáƒ¢áƒ£áƒ¡áƒ˜áƒ¡ ბუფერის სáƒáƒ”ჭვრმდებáƒáƒ áƒ”áƒáƒ‘რ(%li სულ): ხელმის. = %li, დáƒáƒ§áƒáƒ•ნებრ= " +"%li, ბუფერი = %li\n" + +#: aplay/aplay.c:1983 +#, c-format +msgid "" +"Suspicious buffer position avail > delay (%li total): avail = %li, delay = " +"%li\n" +msgstr "" +"ბუფერის სáƒáƒ”ჭვრმდებáƒáƒ áƒ”áƒáƒ‘ის ხელმის. > დáƒáƒ§áƒáƒ•ნებრ(%li სულ): ხელმის. = %li, " +"დáƒáƒ§áƒáƒ•ნებრ= %li\n" + +#: aplay/aplay.c:1987 +#, c-format +msgid "" +"Suspicious status buffer position avail > delay (%li total): avail = %li, " +"delay = %li\n" +msgstr "" +"სტáƒáƒ¢áƒ£áƒ¡áƒ˜áƒ¡ ბუფერის სáƒáƒ”ჭვრმდებáƒáƒ áƒ”áƒáƒ‘ის ხელმის. > დáƒáƒ§áƒáƒ•ნებრ(%li სულ): ხელმის. " +"= %li, დáƒáƒ§áƒáƒ•ნებრ= %li\n" + +#: aplay/aplay.c:2030 +#, c-format +msgid "Status(R/W) (standalone avail=%li delay=%li):\n" +msgstr "სტáƒáƒ¢áƒ£áƒ¡áƒ˜(R/W) (ხელმისáƒáƒ¬áƒ•დ=%li დáƒáƒ§áƒáƒ•ნებáƒ=%li):\n" + +#: aplay/aplay.c:2127 +#, c-format +msgid "write error: %s" +msgstr "ჩáƒáƒ¬áƒ”რის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:2175 +#, c-format +msgid "writev error: %s" +msgstr "writev -ის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:2221 +#, c-format +msgid "read error: %s" +msgstr "read -ის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:2268 +#, c-format +msgid "readv error: %s" +msgstr "readv -ის შეცდáƒáƒ›áƒ: %s" + +#: aplay/aplay.c:2317 +msgid "can't allocate buffer for silence" +msgstr "დáƒáƒ“უმების ბუფერისთვის მეხსიერების გáƒáƒ›áƒáƒ§áƒáƒ¤áƒ˜áƒ¡ შეცდáƒáƒ›áƒ" + +#: aplay/aplay.c:2326 aplay/aplay.c:2554 aplay/aplay.c:2559 aplay/aplay.c:2611 +#: aplay/aplay.c:2620 aplay/aplay.c:2627 aplay/aplay.c:2637 aplay/aplay.c:2643 +#: aplay/aplay.c:2715 aplay/aplay.c:2745 aplay/aplay.c:2759 +msgid "write error" +msgstr "ჩáƒáƒ¬áƒ”რის შეცდáƒáƒ›áƒ" + +#: aplay/aplay.c:2339 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "voc_pcm_flush - დáƒáƒ“უმების სეცდáƒáƒ›áƒ" + +#: aplay/aplay.c:2342 +msgid "voc_pcm_flush error" +msgstr "voc_pcm_flush -ის შეცდáƒáƒ›áƒ" + +#: aplay/aplay.c:2367 +msgid "malloc error" +msgstr "malloc -ის შეცდáƒáƒ›áƒ" + +#: aplay/aplay.c:2371 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "Creative Labs-ი áƒáƒ áƒ®áƒ˜áƒ¡ ფáƒáƒ˜áƒšáƒ˜áƒ¡ \"%s\"-ის დáƒáƒ™áƒ•რáƒ...\n" + +#: aplay/aplay.c:2439 aplay/aplay.c:2534 +msgid "can't play packed .voc files" +msgstr "შეკუმშული .voc ფáƒáƒ˜áƒšáƒ”ბის დáƒáƒ™áƒ•რრშეუძლებელიáƒ" + +#: aplay/aplay.c:2494 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "მáƒáƒ áƒ§áƒ£áƒŸáƒ”ბის დáƒáƒ™áƒ•რის პრáƒáƒ‘ლემáƒ: %s გáƒáƒ“áƒáƒ®áƒ•ევáƒáƒ“ი áƒáƒ áƒáƒ\n" + +#: aplay/aplay.c:2543 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "ბლáƒáƒ™áƒ˜áƒ¡ უცნáƒáƒ‘ი ტიპი %d. მუშáƒáƒáƒ‘ის დáƒáƒ¡áƒáƒ¡áƒ áƒ£áƒšáƒ˜." + +#: aplay/aplay.c:2679 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "Waved-ს %s ფáƒáƒ áƒ›áƒáƒ¢áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რრáƒáƒ  გáƒáƒáƒ©áƒœáƒ˜áƒ..." + +#: aplay/aplay.c:2739 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "Sparc-ის áƒáƒ£áƒ“იáƒáƒ¡ %s ფáƒáƒ áƒ›áƒáƒ¢áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რრáƒáƒ  გáƒáƒáƒ©áƒœáƒ˜áƒ..." + +#: aplay/aplay.c:2814 +msgid "Playing" +msgstr "მიმდინáƒáƒ áƒ”áƒáƒ‘ს დáƒáƒ™áƒ•რáƒ" + +#: aplay/aplay.c:2814 +msgid "Recording" +msgstr "ჩáƒáƒ¬áƒ”რáƒ" + +#: aplay/aplay.c:2818 +#, c-format +msgid "Rate %d Hz, " +msgstr "სიხშირე %d ჰც, " + +#: aplay/aplay.c:2820 +#, c-format +msgid "Mono" +msgstr "მáƒáƒœáƒ" + +#: aplay/aplay.c:2822 +#, c-format +msgid "Stereo" +msgstr "სტერეáƒ" + +#: aplay/aplay.c:2824 +#, c-format +msgid "Channels %i" +msgstr "áƒáƒ áƒ®áƒ”ბი %i" + +#: aplay/aplay.c:3431 +#, c-format +msgid "You need to specify %u files" +msgstr "სáƒáƒ­áƒ˜áƒ áƒáƒ %u ფáƒáƒ˜áƒšáƒ˜áƒ¡ მითითებáƒ" + +#: aplay/aplay.c:3484 +#, c-format +msgid "You need to specify %d files" +msgstr "სáƒáƒ­áƒ˜áƒ áƒáƒ %d ფáƒáƒ˜áƒšáƒ˜áƒ¡ მითითებáƒ" + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - ALSA -ის სეკვენსერთáƒáƒœ მიერთების მმáƒáƒ áƒ—ველი\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "(C) 1999-2000 Takashi Iwai, ყველრუფლებრდáƒáƒªáƒ£áƒšáƒ˜áƒ\n" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "გáƒáƒ›áƒáƒ§áƒ”ნებáƒ:\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * áƒáƒ  პáƒáƒ áƒ¢áƒ¡ შáƒáƒ áƒ˜áƒ¡ კáƒáƒ•შირი/გáƒáƒ—იშვáƒ\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [-პáƒáƒ áƒáƒ›áƒ”ტრები] გáƒáƒ›áƒ’ზáƒáƒ•ნი მიმღები\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " გáƒáƒ›áƒ’ზáƒáƒ•ნი, მიმღები = კლიენტი:პáƒáƒ áƒ¢áƒ˜ წყვილებს\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect disconnecგáƒáƒ—იშვáƒt\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive ექსკლუზიური შეერთებáƒ\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # რიგში real-time-stamp -ის გáƒáƒ áƒ“áƒáƒ¥áƒ›áƒœáƒ\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # რიგში tick-time-stamp -ის გáƒáƒ áƒ“áƒáƒ¥áƒ›áƒœáƒ\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * მიერთებული პáƒáƒ áƒ¢áƒ”ბის სირ(გáƒáƒ›áƒáƒ¬áƒ”რის ქმედების გáƒáƒ áƒ”შე)\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr " aconnect -i|-o [-პáƒáƒ áƒáƒ›áƒ”ტრები]\n" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr " -i,--input შეტáƒáƒœáƒ˜áƒ¡ (წáƒáƒ™áƒ˜áƒ—ხვáƒáƒ“ი) პáƒáƒ áƒ¢áƒ”ბის სიáƒ\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr " -o,--output გáƒáƒ›áƒáƒ¢áƒáƒœáƒ˜áƒ¡ (ჩáƒáƒ¬áƒ”რáƒáƒ“ი) პáƒáƒ áƒ¢áƒ”ბის სიáƒ\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr " -l,--list თითáƒáƒ”ულ პáƒáƒ áƒ¢áƒ–ე მიმდინáƒáƒ áƒ” შეერთებების სიáƒ\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr " * ყველრგáƒáƒ¢áƒáƒœáƒ˜áƒšáƒ˜ შეერთების მáƒáƒªáƒ˜áƒšáƒ”ბáƒ\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr " -x, --removeall\n" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "დáƒáƒ™áƒáƒ•შირების სáƒáƒ›áƒ˜áƒ–ნე" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "დáƒáƒ™áƒáƒ•შირების წყáƒáƒ áƒ" + +#: seq/aconnect/aconnect.c:171 +#, c-format +msgid "client %d: '%s' [type=%s" +msgstr "კლიენტი %d: '%s' [ტიპი=%s" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბელი" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "ბირთვი" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "სეკვენსერის გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "კლიენტის ID-ის მიღების შეცდáƒáƒ\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "კლიენტის ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡ დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "გáƒáƒ›áƒ’ზáƒáƒ•ნის მისáƒáƒ›áƒáƒ áƒ—ის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:303 +#, c-format +msgid "invalid destination address %s\n" +msgstr "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ სáƒáƒ›áƒ˜áƒ–ნე მისáƒáƒ›áƒáƒ áƒ—ი: %s\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "გáƒáƒ›áƒáƒ¬áƒ”რის გáƒáƒ áƒ”შე\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "გáƒáƒ—იშვის შეცდáƒáƒ›áƒ (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "შეერთებრუკვე გáƒáƒ›áƒáƒ¬áƒ”რილიáƒ\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "შეერთების შეცდáƒáƒ›áƒ (%s)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - ქსელური კლიენტ/სერვერი ALSA სეკვენსერზე\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr " (C) 1999-2000 Takashi Iwai\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid "usage:\n" +msgstr "გáƒáƒ›áƒáƒ§áƒ”ნებáƒ:\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " სერვერის რეჟიმი: aseqnet [-პáƒáƒ áƒáƒ›áƒ”ტრები]\n" + +#: seq/aseqnet/aseqnet.c:175 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " კლიენტის რეჟიმი: aseqnet [-პáƒáƒ áƒáƒ›áƒ”ტრები] სერვერის_ჰáƒáƒ¡áƒ¢áƒ˜\n" + +#: seq/aseqnet/aseqnet.c:176 +#, c-format +msgid "options:\n" +msgstr "პáƒáƒ áƒáƒ›áƒ”ტრები:\n" + +#: seq/aseqnet/aseqnet.c:177 +#, c-format +msgid " -6,--ipv6 : use IPv6 TCP protocol\n" +msgstr " -6,--ipv6 : IPv6 TCP პრáƒáƒ¢áƒáƒ™áƒáƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ\n" + +#: seq/aseqnet/aseqnet.c:178 +#, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : TCP პáƒáƒ áƒ¢áƒ˜. (რიცხვი áƒáƒœ სერვისის სáƒáƒ®áƒ”ლი)\n" + +#: seq/aseqnet/aseqnet.c:179 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr "" +" -s,--source მისáƒáƒ›áƒáƒ áƒ—ი : მითითბული მისáƒáƒ›áƒáƒ áƒ—იდáƒáƒœ (კლიენტი:პáƒáƒ áƒ¢áƒ˜) წáƒáƒ™áƒ˜áƒ—ხვáƒ\n" + +#: seq/aseqnet/aseqnet.c:180 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest მისáƒáƒ›áƒáƒ áƒ—ი : მითითებულ მისáƒáƒ›áƒáƒ áƒ—ზე ჩáƒáƒ¬áƒ”რრ(კლიენტი:პáƒáƒ áƒ¢áƒ˜)\n" + +#: seq/aseqnet/aseqnet.c:181 +#, c-format +msgid " -n,--name value : use a specific midi process name\n" +msgstr " -n,--name value : მითითებული MIDI პრáƒáƒªáƒ”სის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ\n" + +#: seq/aseqnet/aseqnet.c:182 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v, --verbose : დáƒáƒ›áƒáƒ¢áƒ”ბითი ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡ გáƒáƒ›áƒáƒ¢áƒáƒœáƒ\n" + +#: seq/aseqnet/aseqnet.c:183 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i, --info : ზáƒáƒ’იერთი მიღებული მáƒáƒ•ლენის გáƒáƒ›áƒáƒ¢áƒáƒœáƒ\n" + +#: seq/aseqnet/aseqnet.c:197 +#, c-format +msgid "can't malloc\n" +msgstr "malloc -ის შეცდáƒáƒ›áƒ\n" + +#: seq/aseqnet/aseqnet.c:222 +#, c-format +msgid "closing files..\n" +msgstr "ფáƒáƒ˜áƒšáƒ”ბის დáƒáƒ®áƒ£áƒ áƒ•áƒ...\n" + +#: seq/aseqnet/aseqnet.c:285 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "სეკვენსერი გáƒáƒ•áƒáƒ¦áƒ”: %d:%d\n" + +#: seq/aseqnet/aseqnet.c:292 +#, c-format +msgid "invalid source address %s\n" +msgstr "წყáƒáƒ áƒáƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ მისáƒáƒ›áƒáƒ áƒ—ი %s\n" + +#: seq/aseqnet/aseqnet.c:362 +#, c-format +msgid "can't get address\n" +msgstr "მისáƒáƒ›áƒáƒ áƒ—ის მიღებრშეუძლებელიáƒ\n" + +#: seq/aseqnet/aseqnet.c:376 seq/aseqnet/aseqnet.c:461 +#, c-format +msgid "connecting to: %s\n" +msgstr "მიერთებáƒ: %s\n" + +#: seq/aseqnet/aseqnet.c:414 +#, c-format +msgid "too many connections!\n" +msgstr "მეტისმეტáƒáƒ“ ბევრი მიერთებáƒ!\n" + +#: seq/aseqnet/aseqnet.c:425 +#, c-format +msgid "accepted[%d]\n" +msgstr "მიღებულიáƒ[%d]\n" + +#: seq/aseqnet/aseqnet.c:447 +#, c-format +msgid "can't get address %s\n" +msgstr "%s-ის მისáƒáƒ›áƒáƒ áƒ—ის მიღების შეცდáƒáƒ›áƒ\n" + +#: seq/aseqnet/aseqnet.c:475 +#, c-format +msgid "ok.. connected\n" +msgstr "შეერთებრწáƒáƒ áƒ›áƒáƒ¢áƒ”ბულიáƒ\n" + +#: seq/aseqnet/aseqnet.c:574 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "áƒáƒ áƒ®áƒ˜ %2d: სáƒáƒ™áƒáƒœáƒ¢áƒ áƒáƒšáƒ მáƒáƒ•ლენრ: %5d\n" + +#: seq/aseqnet/aseqnet.c:578 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "áƒáƒ áƒ®áƒ˜ %2d: Pitchbender : %5d\n" + +#: seq/aseqnet/aseqnet.c:582 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "áƒáƒ áƒ®áƒ˜ %2d: ნáƒáƒ¢áƒ˜áƒ¡ ჩáƒáƒ áƒ—ვის მáƒáƒ•ლენრ: %5d\n" + +#: seq/aseqnet/aseqnet.c:586 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "áƒáƒ áƒ®áƒ˜ %2d: ნáƒáƒ¢áƒ˜áƒ¡ გáƒáƒ›áƒáƒ áƒ—ვის მáƒáƒ•ლენáƒ: %5d\n" + +#: seq/aseqnet/aseqnet.c:641 +#, c-format +msgid "disconnected\n" +msgstr "გáƒáƒ—იშულიáƒ\n" + +#: speaker-test/speaker-test.c:120 +msgid "Front Left" +msgstr "წინრმáƒáƒ áƒªáƒ®áƒ”ნáƒ" + +#: speaker-test/speaker-test.c:121 +msgid "Front Right" +msgstr "წინრმáƒáƒ áƒ¯áƒ•ენáƒ" + +#: speaker-test/speaker-test.c:122 +msgid "Rear Left" +msgstr "უკáƒáƒœáƒ მáƒáƒ áƒªáƒ®áƒ”ნáƒ" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Right" +msgstr "უკáƒáƒœáƒ მáƒáƒ áƒ™áƒ•ენáƒ" + +#: speaker-test/speaker-test.c:125 +msgid "LFE" +msgstr "LFE" + +#: speaker-test/speaker-test.c:126 +msgid "Side Left" +msgstr "მáƒáƒ áƒªáƒ®áƒ”ნრმხáƒáƒ áƒ”ს" + +#: speaker-test/speaker-test.c:127 +msgid "Side Right" +msgstr "მáƒáƒ áƒ¯áƒ•ენრმხáƒáƒ áƒ”ს" + +#: speaker-test/speaker-test.c:128 +msgid "Channel 9" +msgstr "áƒáƒ áƒ®áƒ˜ 9" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 10" +msgstr "áƒáƒ áƒ®áƒ˜ 10" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 11" +msgstr "áƒáƒ áƒ®áƒ˜ 11" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 12" +msgstr "áƒáƒ áƒ®áƒ˜ 12" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 13" +msgstr "áƒáƒ áƒ®áƒ˜ 13" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 14" +msgstr "áƒáƒ áƒ®áƒ˜ 14" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 15" +msgstr "áƒáƒ áƒ®áƒ˜ 15" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 16" +msgstr "áƒáƒ áƒ®áƒ˜ 16" + +#: speaker-test/speaker-test.c:440 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "დáƒáƒ™áƒ•რის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ დáƒáƒ–იáƒáƒœáƒ”ბულიáƒ: კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ გáƒáƒ áƒ”შე: %s\n" + +#: speaker-test/speaker-test.c:447 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "წვდáƒáƒ›áƒ˜áƒ¡ ტიპი დáƒáƒ¡áƒáƒ™áƒ áƒáƒ•áƒáƒ“ ხელმიუწვდáƒáƒ›áƒ”ლიáƒ: %s\n" + +#: speaker-test/speaker-test.c:454 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "სემპლის ტიპი დáƒáƒ¡áƒáƒ™áƒ áƒáƒ•áƒáƒ“ ხელმიუწვდáƒáƒ›áƒ”ლიáƒ: %s\n" + +#: speaker-test/speaker-test.c:461 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "áƒáƒ áƒ®áƒ”ბის რáƒáƒáƒ“ენáƒáƒ‘რ(%i) დáƒáƒ¡áƒáƒ™áƒ áƒáƒ•áƒáƒ“ ხელმიუწვდáƒáƒ›áƒ”ლიáƒ: %s\n" + +#: speaker-test/speaker-test.c:469 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "სიხშირე %iჰც დáƒáƒ¡áƒáƒ™áƒ áƒáƒ•áƒáƒ“ მიუწვდáƒáƒ›áƒ”ლიáƒ: %s\n" + +#: speaker-test/speaker-test.c:474 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "სიხშირე áƒáƒ  ემთხვევრ(მáƒáƒ—ხáƒáƒ•ნილი %iჰც, მიღებული %iჰც, შეცდáƒáƒ›áƒ %d)\n" + +#: speaker-test/speaker-test.c:478 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "სიხშირე დáƒáƒ§áƒ”ნებულირ%iჰც-ზე (მáƒáƒ—ხáƒáƒ•ნილირ%iჰც)\n" + +#: speaker-test/speaker-test.c:484 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "ბუფერის ზáƒáƒ›áƒ˜áƒ¡ დიáƒáƒžáƒáƒ–áƒáƒœáƒ˜áƒ %lu-დáƒáƒœ %lu-მდე\n" + +#: speaker-test/speaker-test.c:485 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "" +"პერიáƒáƒ“ის ზáƒáƒ›áƒ˜áƒ¡ დიáƒáƒžáƒáƒ–áƒáƒœáƒ˜áƒ %lu-დáƒáƒœ %lu-მდე\n" +"\n" + +#: speaker-test/speaker-test.c:487 +#, c-format +msgid "Requested period time %u us\n" +msgstr "მáƒáƒ—ხáƒáƒ•ნილი პერიáƒáƒ“ის დრáƒáƒ %u მიკრáƒáƒ¬áƒáƒ›áƒ˜\n" + +#: speaker-test/speaker-test.c:490 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის პერიáƒáƒ“ის დრáƒáƒ“ %u მკწმ-ის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:496 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "მáƒáƒ—ხáƒáƒ•ნილი ბუფერის დრáƒáƒ %u მიკრáƒáƒ¬áƒáƒ›áƒ˜\n" + +#: speaker-test/speaker-test.c:499 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის ბáƒáƒ¤áƒ”რის დრáƒáƒ“ %u მკწმ-ის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:508 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებრბუფერის მáƒáƒ¥áƒ¡áƒ˜áƒ›áƒáƒšáƒ£áƒ áƒ˜ ზáƒáƒ›áƒ %lu\n" + +#: speaker-test/speaker-test.c:511 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის ბáƒáƒ¤áƒ”რის ზáƒáƒ›áƒáƒ“ %lu-ის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:517 +#, c-format +msgid "Periods = %u\n" +msgstr "პერიáƒáƒ“ები = %u\n" + +#: speaker-test/speaker-test.c:520 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის nperiods-ის %u-ზე დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:529 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:535 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "period_size დáƒáƒ§áƒ”ნებულ იყáƒ= %lu\n" + +#: speaker-test/speaker-test.c:536 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "buffer_size დáƒáƒ§áƒ”ნებული იყრ= %lu\n" + +#: speaker-test/speaker-test.c:538 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "ბáƒáƒ¤áƒ”რი მეტისმეტáƒáƒ“ პáƒáƒ¢áƒáƒ áƒáƒ. ვერ გáƒáƒ›áƒáƒ•იყენებ\n" + +#: speaker-test/speaker-test.c:551 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის მიმდინáƒáƒ áƒ” პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ“გენის შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:558 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის დáƒáƒ¡áƒáƒ¬áƒ§áƒ˜áƒ¡áƒ˜áƒ¡ ზღვáƒáƒ áƒ˜áƒ¡ რეჟიმის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:565 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜ მინიმუმის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:572 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "დáƒáƒ™áƒ•რისთვის პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:592 +#, c-format +msgid "Unable to set channel map: %s\n" +msgstr "áƒáƒ áƒ®áƒ”ბის რუკის დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:617 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "ბáƒáƒ¤áƒ”რის ვერშევსებიდáƒáƒœ áƒáƒ¦áƒ“გენრშეუძლებელიáƒ. მáƒáƒ›áƒ–áƒáƒ“ების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:628 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "შეჩერებიდáƒáƒœ áƒáƒ¦áƒ“გენრშეუძლებელიáƒ. მáƒáƒ›áƒ–áƒáƒ“ების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:692 speaker-test/speaker-test.c:1207 +#, c-format +msgid "No enough memory\n" +msgstr "áƒáƒ áƒáƒ¡áƒáƒ™áƒ›áƒáƒ áƒ˜áƒ¡áƒ˜ მეხსიერებáƒ\n" + +#: speaker-test/speaker-test.c:697 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "WAV ფáƒáƒ˜áƒšáƒ˜áƒ¡ გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:701 speaker-test/speaker-test.c:730 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ WAV ფáƒáƒ˜áƒšáƒ˜: %s\n" + +#: speaker-test/speaker-test.c:706 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ WAV ფáƒáƒ˜áƒšáƒ˜: %s\n" + +#: speaker-test/speaker-test.c:710 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "WAV ფáƒáƒ˜áƒšáƒ˜áƒ¡ áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ფáƒáƒ áƒ›áƒáƒ¢áƒ˜ \"%d\" \"%s\"-სთვის\n" + +#: speaker-test/speaker-test.c:715 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s მáƒáƒœáƒ ნáƒáƒ™áƒáƒ“ს áƒáƒ  წáƒáƒ áƒ›áƒáƒáƒ“გენს (%d áƒáƒ áƒ®áƒ˜)\n" + +#: speaker-test/speaker-test.c:720 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "სემპლინგის სიჩქáƒáƒ áƒ” áƒáƒ  ემთხვევრ(%d) %s-სთვის\n" + +#: speaker-test/speaker-test.c:725 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "სემპლის მხáƒáƒ áƒ“áƒáƒ£áƒ­áƒ”რელი ბიტები (%d) %s-სთვის\n" + +#: speaker-test/speaker-test.c:786 +#, c-format +msgid "Undefined channel %d\n" +msgstr "áƒáƒ¦áƒ£áƒ¬áƒ”რელი áƒáƒ áƒ®áƒ˜ %d\n" + +#: speaker-test/speaker-test.c:837 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "ჩáƒáƒ¬áƒ”რის შეცდáƒáƒ›áƒ: %d,%s\n" + +#: speaker-test/speaker-test.c:839 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery -ის შეცდáƒáƒ›áƒ: %d,%s\n" + +#: speaker-test/speaker-test.c:942 +#, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"გáƒáƒ›áƒáƒ§áƒ”ნებáƒ: speaker-test [პáƒáƒ áƒáƒ›áƒ”ტრი]... \n" +"-h,--help\tდáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ\n" +"-D,--device\tდáƒáƒ›áƒ™áƒ•რელი მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ\n" +"-r,--rate\tნáƒáƒ™áƒáƒ“ის სიხშირე ჰერცებში\n" +"-c,--channels\táƒáƒ áƒ®áƒ”ბის რáƒáƒáƒ“ენáƒáƒ‘რნáƒáƒ™áƒáƒ“ში\n" +"-f,--frequency\tსინუსáƒáƒ˜áƒ“áƒáƒšáƒ£áƒ áƒ˜ ტáƒáƒšáƒ¦áƒ˜áƒ¡ სიხშირე ჰერცებში\n" +"-F,--format\tსემპლის ფáƒáƒ áƒ›áƒáƒ¢áƒ˜\n" +"-b,--buffer\tბეჭდის ბáƒáƒ¤áƒ”რის ზáƒáƒ›áƒ მიკრáƒáƒ¬áƒáƒ›áƒ”ბში\n" +"-p,--period\tპერიáƒáƒ“ის ზáƒáƒ›áƒ მიკრáƒáƒ¬áƒáƒ›áƒ”ბში\n" +"-P,--nperiods\tპერიáƒáƒ“ების რიცხვი\n" +"-t,--test\tpink=ვáƒáƒ áƒ“ისფერი ხმáƒáƒ£áƒ áƒ˜, sine=სინუსáƒáƒ˜áƒ“áƒáƒšáƒ£áƒ áƒ˜ ტáƒáƒšáƒ¦áƒ, wav=WAV ფáƒáƒ˜áƒšáƒ˜\n" +"-l,--nloops\tსáƒáƒ¢áƒ”სტრმáƒáƒ áƒ§áƒ£áƒŸáƒ”ბის რáƒáƒáƒ“ენáƒáƒ‘áƒ, 0 = უსáƒáƒ¡áƒ áƒ£áƒšáƒ\n" +"-s,--speaker\tერთი დინáƒáƒ›áƒ˜áƒ™áƒ˜áƒ¡ ტესტი. მნიშვნელáƒáƒ‘ები 1=მáƒáƒ áƒªáƒ®áƒ”ნáƒ, 2=მáƒáƒ áƒ¯áƒ•ენáƒ, " +"დრáƒ.შ.\n" +"-w,--wavfile\tსáƒáƒ¢áƒ”სტრხმáƒáƒ“ მითითებული WAV ფáƒáƒ˜áƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ\n" +"-W,--wavdir\t WAV ფáƒáƒ˜áƒšáƒ”ბის შემცველი სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ე\n" +"-m,--chmap\tგáƒáƒ“áƒáƒ¡áƒáƒ¤áƒáƒ áƒ˜ áƒáƒ áƒ®áƒ”ბის რუკáƒ\n" +"-X,--force-frequency\tსიხშირის ძáƒáƒšáƒ˜áƒ— მითითებრ30-8000ჰც დიáƒáƒžáƒáƒ–áƒáƒœáƒ˜áƒ¡ გáƒáƒ áƒ”დáƒáƒœ\n" +"-S,--scale\tსáƒáƒ¢áƒ”სტრტáƒáƒœáƒ”ბის გáƒáƒ«áƒšáƒ˜áƒ”რებრპრáƒáƒªáƒ”ნტებში (ნáƒáƒ’ულისხმები=80)\n" +"\n" + +#: speaker-test/speaker-test.c:1074 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "პერიáƒáƒ“ების áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ რიცხვი: %d\n" + +#: speaker-test/speaker-test.c:1090 speaker-test/speaker-test.c:1094 +#, c-format +msgid "Invalid test type %s\n" +msgstr "ტესტის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ტიპი %s\n" + +#: speaker-test/speaker-test.c:1127 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "უცნáƒáƒ‘ი პáƒáƒ áƒáƒ›áƒ”ტრი '%c'\n" + +#: speaker-test/speaker-test.c:1141 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "-s პáƒáƒ áƒáƒ›áƒ”ტრის áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ მნიშვნელáƒáƒ‘áƒ.\n" + +#: speaker-test/speaker-test.c:1156 +#, c-format +msgid "Playback device is %s\n" +msgstr "დáƒáƒ›áƒ™áƒ•რელი მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ %s\n" + +#: speaker-test/speaker-test.c:1157 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "ნáƒáƒ™áƒáƒ“ის პáƒáƒ áƒáƒ›áƒ”ტრებირ%iჰც, %s, %i áƒáƒ áƒ®áƒ˜\n" + +#: speaker-test/speaker-test.c:1160 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "ვáƒáƒ áƒ“ისფერი ხმáƒáƒ£áƒ áƒ˜áƒ¡áƒ—ვის 16 áƒáƒ¥áƒ¢áƒáƒ•ის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ\n" + +#: speaker-test/speaker-test.c:1163 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "სინუსáƒáƒ˜áƒ“áƒáƒšáƒ£áƒ áƒ˜ ტáƒáƒšáƒ¦áƒ˜áƒ¡ სიხშირე %.4fჰც-áƒ\n" + +#: speaker-test/speaker-test.c:1166 +#, c-format +msgid "WAV file(s)\n" +msgstr "WAV ფáƒáƒ˜áƒš(ებ)-ი\n" + +#: speaker-test/speaker-test.c:1176 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "დáƒáƒ™áƒ•რის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის გáƒáƒ®áƒ¡áƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ: %d,%s\n" + +#: speaker-test/speaker-test.c:1182 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:1186 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ პáƒáƒ áƒáƒ›áƒ”ტრების დáƒáƒ§áƒ”ნების შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:1232 speaker-test/speaker-test.c:1254 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "გáƒáƒ“áƒáƒ¢áƒáƒœáƒ˜áƒ¡ შეცდáƒáƒ›áƒ: %s\n" + +#: speaker-test/speaker-test.c:1240 +#, c-format +msgid "Time per period = %lf\n" +msgstr "დრრთითáƒáƒ”ულ პერიáƒáƒ“ში = %lf\n" diff --git a/po/ko.po b/po/ko.po new file mode 100644 index 0000000..23b5520 --- /dev/null +++ b/po/ko.po @@ -0,0 +1,1684 @@ +# Basque translation for alsa-utils +# Copyright (C) 2011 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Porrumentzio , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.23\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-10 09:07+0100\n" +"PO-Revision-Date: 2023-05-25 17:58+0900\n" +"Language-Team: Librezale \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" +"Last-Translator: Yi Yunseok \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Language: ko\n" + +#: alsamixer/card_select.c:126 alsamixer/device_name.c:126 +msgid "Sound Card" +msgstr "사운드 카드" + +#: alsamixer/card_select.c:181 +msgid "(default)" +msgstr "(기본)" + +#: alsamixer/card_select.c:191 +msgid "cannot enumerate sound cards" +msgstr "사운드 카드를 ì—´ê±°í•  수 ì—†ìŒ" + +#: alsamixer/card_select.c:215 +msgid "enter device name..." +msgstr "장치 ì´ë¦„ ìž…ë ¥..." + +#: alsamixer/cli.c:40 +msgid "Usage: alsamixer [options]" +msgstr "사용법: alsamixer [options]" + +# capture --> atzemate, hartze, kaptura(tze)...? +#: alsamixer/cli.c:41 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"유용한 옵션:\n" +" -h, --help ë„움ë§\n" +" -c, --card=NUMBER 사운드 카드 번호 ë˜ëŠ” id\n" +" -D, --device=NAME 믹서 장치 ì´ë¦„\n" +" -V, --view=MODE 보기 모드 시작: 재ìƒ/캡처/모ë‘" + +#: alsamixer/cli.c:46 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"디버깅 옵션:\n" +" -g, --no-color ìƒ‰ìƒ ì‚¬ìš© 전환\n" +" -a, --abstraction=NAME 믹서 ì¶”ìƒ ë ˆë²¨: ì—†ìŒ/기본" + +#: alsamixer/cli.c:77 +#, c-format +msgid "invalid card index: %s\n" +msgstr "ìž˜ëª»ëœ ì¹´ë“œ ì¸ë±ìФ: %s\n" + +#: alsamixer/cli.c:103 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "알 수 없는 ì¶”ìƒ ë ˆë²¨: %s\n" + +#: alsamixer/cli.c:108 +#, c-format +msgid "unknown option: %c\n" +msgstr "알 수 없는 옵션: %c\n" + +#: alsamixer/cli.c:110 +msgid "try `alsamixer --help' for more information\n" +msgstr "`alsamixer --help' 명령으로 ë” ë§Žì€ ì •ë³´ 확ì¸\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "장치 ì´ë¦„:" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "%s: %s\n" + +#: alsamixer/mixer_display.c:98 +msgid "Card:" +msgstr "카드:" + +#: alsamixer/mixer_display.c:99 +msgid "Chip:" +msgstr "칩:" + +#: alsamixer/mixer_display.c:100 +msgid "View:" +msgstr "보기:" + +#: alsamixer/mixer_display.c:101 +msgid "Item:" +msgstr "ë„구:" + +#: alsamixer/mixer_display.c:104 +msgid "F1: Help" +msgstr "F1: ë„움ë§" + +#: alsamixer/mixer_display.c:105 +msgid "F2: System information" +msgstr "F2: 시스템 ì •ë³´" + +#: alsamixer/mixer_display.c:106 +msgid "F6: Select sound card" +msgstr "F6: 사운드 카드 ì„ íƒ" + +# Esc --> Ihes / Esc +#: alsamixer/mixer_display.c:107 +msgid "Esc: Exit" +msgstr "Esc: 나가기" + +#: alsamixer/mixer_display.c:174 +msgid "(unplugged)" +msgstr "(ì—°ê²°ë˜ì§€ 않ìŒ)" + +#: alsamixer/mixer_display.c:192 +msgid "Playback" +msgstr "재ìƒ" + +#: alsamixer/mixer_display.c:193 +msgid "Capture" +msgstr "캡처" + +#: alsamixer/mixer_display.c:194 +msgid "All" +msgstr "모ë‘" + +#: alsamixer/mixer_display.c:234 +msgid "mute" +msgstr "ìŒì†Œê±°" + +#: alsamixer/mixer_display.c:275 alsamixer/mixer_display.c:285 +msgid "dB gain:" +msgstr "dB ì¦í­:" + +#: alsamixer/mixer_display.c:285 +#, c-format +msgid " [%s %s, %s]" +msgstr " [%s %s, %s]" + +#: alsamixer/mixer_display.c:294 alsamixer/mixer_display.c:300 +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:312 +msgid "Off" +msgstr "ë„기" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:312 +msgid "On" +msgstr "켜기" + +#: alsamixer/mixer_display.c:363 +msgid "The sound device was unplugged." +msgstr "사운드 장치 ì—°ê²°ì´ í•´ì œë습니다." + +#: alsamixer/mixer_display.c:364 +msgid "Press F6 to select another sound card." +msgstr "F6ì„ ëˆŒëŸ¬ 다른 사운드 카드를 ì„ íƒí•˜ì„¸ìš”." + +#: alsamixer/mixer_display.c:379 +msgid "This sound device does not have any playback controls." +msgstr "ì´ ì‚¬ìš´ë“œ 장치는 ìž¬ìƒ ì œì–´ë¥¼ í•  수 없습니다." + +#: alsamixer/mixer_display.c:381 +msgid "This sound device does not have any capture controls." +msgstr "ì´ ì‚¬ìš´ë“œ 장치는 캡처 제어를 í•  수 없습니다." + +#: alsamixer/mixer_display.c:383 +msgid "This sound device does not have any controls." +msgstr "ì´ ì‚¬ìš´ë“œ 장치는 제어할 수 없습니다." + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:516 alsamixer/mixer_display.c:521 +msgid "O" +msgstr "O" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:518 alsamixer/mixer_display.c:522 +msgid "M" +msgstr "M" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:536 +msgid "L" +msgstr "L" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:540 +msgid "R" +msgstr "R" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:542 +msgid "CAPTURE" +msgstr "캡처" + +#: alsamixer/mixer_display.c:592 +msgid "Front" +msgstr "ì „ë©´" + +#: alsamixer/mixer_display.c:595 +msgid "Rear" +msgstr "후면" + +#: alsamixer/mixer_display.c:598 speaker-test/speaker-test.c:125 +msgid "Center" +msgstr "중앙" + +#: alsamixer/mixer_display.c:601 +msgid "Woofer" +msgstr "ìš°í¼" + +#: alsamixer/mixer_display.c:604 +msgid "Side" +msgstr "측면" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "믹서를 ì—´ 수 ì—†ìŒ" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "믹서 ì»¨íŠ¸ë¡¤ì„ ë¶ˆëŸ¬ì˜¬ 수 ì—†ìŒ" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "'%s' 믹서 장치를 ì—´ 수 없습니다." + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc 나가기" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H ë„움ë§" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / 시스템 ì •ë³´" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 ìž¬ìƒ ì»¨íŠ¸ë¡¤ 표시" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 캡처 컨트롤 표시" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 모든 컨트롤 표시" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab 보기 모드 전환 (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F6 S 사운드 카드 ì„ íƒ" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L 화면 새로고침" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "Left ì´ì „ 컨트롤로 ì´ë™" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "Right ë‹¤ìŒ ì»¨íŠ¸ë¡¤ë¡œ ì´ë™" + +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "Up/Down ìŒëŸ‰ 변경" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - ìŒëŸ‰ 변경" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "Page Up/Dn ìŒëŸ‰ì„ í° í­ìœ¼ë¡œ 변경" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "End ìŒëŸ‰ì„ 0%로 설정" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 ìŒëŸ‰ì„ 0%-90%로 설정" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E 왼쪽/ì „ì²´/오른쪽 ìŒëŸ‰ 늘리기" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Z X C 왼쪽/ì „ì²´/오른쪽 ìŒëŸ‰ 줄ì´ê¸°" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B 좌우 ìŒëŸ‰ 밸런스" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M ìŒì†Œê±° 전환" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr "< > 좌/ìš° ìŒì†Œê±° 전환" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "Space 캡처 전환" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "; ' 좌/ìš° 캡처 전환" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "ì €ìž:" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr " 팀 야닉(Tim Janik)" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr " 야로슬ë¼í”„ 키셀ë¼(Jaroslav Kysela) " + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr " í´ë ˆë©˜ìФ ë¼ë””쉬(Clemens Ladisch) " + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "ë„움ë§" + +#: alsamixer/proc_files.c:103 +msgid "Select File" +msgstr "íŒŒì¼ ì„ íƒ" + +#: alsamixer/textbox.c:52 alsamixer/textbox.c:66 +msgid "Error" +msgstr "오류" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "\"%s\" 파ì¼ì„ ì—´ 수 없습니다." + +#: aplay/aplay.c:178 +msgid "raw data" +msgstr "raw ë°ì´í„°" + +#: aplay/aplay.c:179 +msgid "VOC" +msgstr "VOC" + +#: aplay/aplay.c:181 +msgid "WAVE" +msgstr "WAVE" + +#: aplay/aplay.c:182 +msgid "Sparc Audio" +msgstr "ìŠ¤íŒŒí¬ ì˜¤ë””ì˜¤" + +#: aplay/aplay.c:203 +#, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file " +"name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"사용법: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help ë„움ë§\n" +" --version 현재 버전 출력\n" +"-l, --list-devices 모든 디지털 오디오 장치와 사운드 카드 표시\n" +"-L, --list-pcms 장치 ì´ë¦„ 목ë¡\n" +"-D, --device=NAME PCM ì´ë¦„차순\n" +"-q, --quiet 조용한 모드\n" +"-t, --file-type TYPE íŒŒì¼ ìœ í˜• (voc, wav, raw ë˜ëŠ” au)\n" +"-c, --channels=# 채ë„\n" +"-f, --format=FORMAT 샘플 í¬ë§· (ëŒ€ì†Œë¬¸ìž êµ¬ë¶„í•˜ì§€ 않ìŒ)\n" +"-r, --rate=# ìƒ˜í”Œë§ ì†ë„\n" +"-d, --duration=# # ì´ˆ ì´í›„ ì¸í„°ëŸ½íЏ\n" +"-s, --samples=# # ì±„ë„ ë‹¹ ìƒ˜í”Œë§ í›„ ì¸í„°ëŸ½íЏ\n" +"-M, --mmap mmap 스트림\n" +"-N, --nonblock 비차단 모드\n" +"-F, --period-time=# # 마ì´í¬ë¡œì´ˆ 사ì´ë¡œ ì¸í„°ëŸ½íЏ\n" +"-B, --buffer-time=# # ë²„í¼ ì§€ì† ë§ˆì´í¬ë¡œì´ˆ\n" +" --period-size=# # 프레임 사ì´ë¡œ ì¸í„°ëŸ½íЏ\n" +" --buffer-size=# # ë²„í¼ ì§€ì† í”„ë ˆìž„\n" +"-A, --avail-min=# # 최소 웨ì´í¬ì—… 마ì´í¬ë¡œì´ˆ\n" +"-R, --start-delay=# # ìžë™ PCM 시작 지연 마ì´í¬ë¡œì´ˆ \n" +" (ë²„í¼ ìƒëŒ€ì  í¬ê¸° <= 0)\n" +"-T, --stop-delay=# # xrun ìžë™ PCM 중지 지연 시간\n" +"-v, --verbose PCM 구조 ë° ì„¤ì • 표시(누ì )\n" +"-V, --vumeter=TYPE VU 미터 표시 (형태: 모노 ë˜ëŠ” 스테레오)\n" +"-I, --separate-channels ì±„ë„ ë‹¹ í•˜ë‚˜ì˜ íŒŒì¼\n" +"-i, --interactive stdin으로부터 ì¸í„°ë ‰í‹°ë¸Œ 작업 허용\n" +"-m, --chmap=ch1,ch2,.. ë®ì–´ì“°ê±°ë‚˜ 따를 ì±„ë„ ë§µ 제공\n" +" --disable-resample ìžë™ 리샘플 ì†ë„ 비활성\n" +" --disable-channels ìžë™ ì±„ë„ ë³€í™˜ 비활성\n" +" --disable-format ìžë™ í¬ë§· 변환 비활성\n" +" --disable-softvol 소프트웨어 ìŒëŸ‰ 제어(softvol) 비활성\n" +" --test-position ë§ ë²„í¼ ìœ„ì¹˜ 테스트\n" +" --test-coef=# ë§ ë²„í¼ ìœ„ì¹˜ 계수 테스트 (기본 8)\n" +" ê²€ì¦ í‘œí˜„ì‹: coef * (buffer_size / 2)\n" +" --test-nowait ë§ ë²„í¼ë¥¼ 기다리지 ì•ŠìŒ - CPU 소모\n" +" --max-file-time=# ì´ì „ 파ì¼ì´ 기ë¡ë˜ë©´ 다른 출력 íŒŒì¼ ì‹œìž‘\n" +" ì´ ìˆ˜ì´ˆ ë™ì•ˆ\n" +" --process-id-file ì—¬ê¸°ì— í”„ë¡œì„¸ìŠ¤ ID ìž…ë ¥\n" +" --use-strftime 출력 íŒŒì¼ ì´ë¦„ì— strftime 기능 ì ìš©\n" +" --dump-hw-params ìž¥ì¹˜ì˜ hw_params ë¤í”„\n" +" --fatal-errors 모든 오류를 ì¹˜ëª…ì  ì˜¤ë¥˜ë¡œ 취급\n" + +#: aplay/aplay.c:248 speaker-test/speaker-test.c:1023 +#, c-format +msgid "Recognized sample formats are:" +msgstr "ì¸ì‹ë˜ëŠ” 샘플 형ì‹:" + +#: aplay/aplay.c:254 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"ì´ ì¤‘ ì¼ë¶€ëŠ” ì„ íƒí•œ 하드웨어ì—서 사용 불가능\n" + +#: aplay/aplay.c:255 +#, c-format +msgid "The available format shortcuts are:\n" +msgstr "사용 가능한 í¬ë§· 단축키:\n" + +#: aplay/aplay.c:256 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16비트 리틀 엔디언, 44100, 스테레오)\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16비트 ë¹… 엔디언, 44100, 스테레오)\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16비트 리틀 엔디언, 48000, 스테레오)\n" + +#: aplay/aplay.c:272 +msgid "no soundcards found..." +msgstr "사운드 카드를 ì°¾ì„ ìˆ˜ ì—†ìŒ..." + +#: aplay/aplay.c:275 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** %s 하드웨어 장치 ëª©ë¡ ****\n" + +#: aplay/aplay.c:304 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "카드 %i: %s [%s], 장치 %i: %s [%s]\n" + +#: aplay/aplay.c:310 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " 주변장치: %i/%i\n" + +#: aplay/aplay.c:317 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " 주변장치 #%i: %s\n" + +#: aplay/aplay.c:398 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "%s 신호로 ì¸í•´ 중단ë¨...\n" + +#: aplay/aplay.c:553 +msgid "command should be named either arecord or aplay" +msgstr "ëª…ë ¹ì€ aplay ë˜ëŠ” arecord로 지정해야 함" + +#: aplay/aplay.c:597 +#, c-format +msgid "unrecognized file format %s" +msgstr "ì¸ì‹í•  수 없는 íŒŒì¼ í˜•ì‹ %s" + +#: aplay/aplay.c:604 +#, c-format +msgid "invalid channels argument '%s'" +msgstr "ìž˜ëª»ëœ ì±„ë„ ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:608 +#, c-format +msgid "value %i for channels is invalid" +msgstr "채ë„ì˜ %i ê°’ì´ ìž˜ëª»ë¨" + +#: aplay/aplay.c:627 +#, c-format +msgid "wrong extended format '%s'" +msgstr "ìž˜ëª»ëœ í™•ìž¥ í˜•ì‹ '%s'" + +#: aplay/aplay.c:635 +#, c-format +msgid "invalid rate argument '%s'" +msgstr "ìž˜ëª»ëœ ì†ë„ ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:642 +#, c-format +msgid "bad speed value %i" +msgstr "ìž˜ëª»ëœ ì†ë„ ê°’ %i" + +#: aplay/aplay.c:648 +msgid "duration and samples arguments cannot be used together" +msgstr "샘플ë§ê³¼ ì§€ì†ì‹œê°„ ì¸ìˆ˜ëŠ” 함께 사용할 수 ì—†ìŒ" + +#: aplay/aplay.c:653 +#, c-format +msgid "invalid duration argument '%s'" +msgstr "ìž˜ëª»ëœ ì§€ì†ì‹œê°„ ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:660 +msgid "samples and duration arguments cannot be used together" +msgstr "ì§€ì† ì‹œê°„ê³¼ ìƒ˜í”Œë§ ì¸ìˆ˜ëŠ” 함께 사용할 수 ì—†ìŒ" + +#: aplay/aplay.c:665 +#, c-format +msgid "invalid samples argument '%s'" +msgstr "ìž˜ëª»ëœ ìƒ˜í”Œë§ ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:677 +#, c-format +msgid "invalid period time argument '%s'" +msgstr "ìž˜ëª»ëœ ì£¼ê¸° 시간 ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:684 +#, c-format +msgid "invalid buffer time argument '%s'" +msgstr "ìž˜ëª»ëœ ë²„í¼ ì‹œê°„ ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:691 +#, c-format +msgid "invalid period size argument '%s'" +msgstr "ìž˜ëª»ëœ ì£¼ê¸° í¬ê¸° ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:698 +#, c-format +msgid "invalid buffer size argument '%s'" +msgstr "ìž˜ëª»ëœ ë²„í¼ í¬ê¸° ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:705 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "ìž˜ëª»ëœ ìµœì†Œ 사용 가능 공간 ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:712 +#, c-format +msgid "invalid start delay argument '%s'" +msgstr "ìž˜ëª»ëœ ì‹œìž‘ 지연 ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:719 +#, c-format +msgid "invalid stop delay argument '%s'" +msgstr "ìž˜ëª»ëœ ì¤‘ì§€ 지연 ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:774 +#, c-format +msgid "invalid test coef argument '%s'" +msgstr "ìž˜ëª»ëœ í…ŒìŠ¤íŠ¸ 계수 ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:786 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "ìž˜ëª»ëœ ìµœëŒ€ íŒŒì¼ ì‹œê°„ ì¸ìˆ˜ '%s'" + +#: aplay/aplay.c:806 speaker-test/speaker-test.c:666 +#, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "ì±„ë„ ë§µ 문ìžì—´ 구문 ë¶„ì„ ë¶ˆê°€: %s\n" + +#: aplay/aplay.c:812 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "`%s --help' 명령으로 ë” ë§Žì€ ì •ë³´ë¥¼ 확ì¸í•  수 있습니다.\n" + +#: aplay/aplay.c:828 +#, c-format +msgid "audio open error: %s" +msgstr "오디오 열기 오류: %s" + +#: aplay/aplay.c:833 +#, c-format +msgid "info error: %s" +msgstr "ì •ë³´ 오류: %s" + +#: aplay/aplay.c:840 +#, c-format +msgid "nonblock setting error: %s" +msgstr "ë¹„ë¸”ë¡ ì„¤ì • 오류: %s" + +#: aplay/aplay.c:850 aplay/aplay.c:974 aplay/aplay.c:1264 aplay/aplay.c:1458 +#: aplay/aplay.c:1979 aplay/aplay.c:2009 +msgid "not enough memory" +msgstr "메모리 부족" + +#: aplay/aplay.c:874 +#, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "%s 프로세스 ID 파ì¼ì„ ìƒì„±í•  수 ì—†ìŒ: %s" + +#: aplay/aplay.c:964 +#, c-format +msgid "read error (called from line %i)" +msgstr "ì½ê¸° 오류 (%i í–‰ì—서 호출ë¨)" + +#: aplay/aplay.c:1031 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "알 수 없는 'fmt ' ì²­í¬ í¬ê¸° (%u ì½ê¸°ëŠ” 최소한 %u ê°’ê³¼ 같아야 함)" + +#: aplay/aplay.c:1042 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"알 수 없는 'fmt ' 확장 가능 ì²­í¬ í¬ê¸° (%u ì½ê¸°ëŠ” 최소한 %u ê°’ê³¼ 같아야 함)" + +#: aplay/aplay.c:1047 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "ìž˜ëª»ëœ 'fmt ' 확장 가능 ì²­í¬ í¬ë§· 태그" + +#: aplay/aplay.c:1054 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "재ìƒí•  수 없는 PCM ë˜ëŠ” FLOAT로 ì¸ì½”딩ë˜ì§€ ì•Šì€ WAVE íŒŒì¼ í˜•ì‹ 0x%04x" + +#: aplay/aplay.c:1059 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "%d íŠ¸ëž™ì˜ WAVE íŒŒì¼ ìž¬ìƒ ë¶ˆê°€" + +#: aplay/aplay.c:1067 aplay/aplay.c:1193 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "주ì˜: 형ì‹ì´ U8로 변경ë¨\n" + +#: aplay/aplay.c:1077 aplay/aplay.c:1090 aplay/aplay.c:1101 +#, c-format +msgid "Warning: format is changed to %s\n" +msgstr "주ì˜: 형ì‹ì´ %s로 변경ë¨\n" + +#: aplay/aplay.c:1106 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr " %d ë°”ì´íЏ í­ì—서 %d 비트 WAVE 샘플 íŒŒì¼ ìž¬ìƒ ë¶ˆê°€ (%d 채ë„)" + +#: aplay/aplay.c:1129 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr " %d 비트 í­ì˜ WAVE 샘플 íŒŒì¼ ìž¬ìƒ ë¶ˆê°€" + +#: aplay/aplay.c:1187 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "주ì˜: í¬ë§·ì´ MU_LAW로 변경ë¨\n" + +#: aplay/aplay.c:1199 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "주ì˜: í¬ë§·ì´ S16_BE로 변경ë¨\n" + +#: aplay/aplay.c:1212 aplay/aplay.c:2294 aplay/aplay.c:2301 aplay/aplay.c:2831 +msgid "read error" +msgstr "ì½ê¸° 오류" + +#: aplay/aplay.c:1242 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "ì±„ë„ ë§µê³¼ hw_param ê°„ ì±„ë„ ë²ˆí˜¸ê°€ ì¼ì¹˜í•˜ì§€ 않ìŒ" + +#: aplay/aplay.c:1251 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "주ì˜: ì±„ë„ ë§µì„ ê°€ì ¸ì˜¬ 수 ì—†ìŒ\n" + +#: aplay/aplay.c:1284 +#, c-format +msgid "Channel %d doesn't match with hw_parmas" +msgstr "ì±„ë„ %d ë° hw_paramsê°€ ì¼ì¹˜í•˜ì§€ 않ìŒ" + +#: aplay/aplay.c:1310 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "ì†ìƒëœ PCM 구성: 사용 가능한 êµ¬ì„±ì´ ì—†ìŒ" + +#: aplay/aplay.c:1314 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "\"%s\" ìž¥ì¹˜ì˜ í•˜ë“œì›¨ì–´ 매개변수:\n" + +#: aplay/aplay.c:1334 +msgid "Access type not available" +msgstr "액세스 ìœ í˜•ì„ ì‚¬ìš©í•  수 ì—†ìŒ" + +#: aplay/aplay.c:1339 +msgid "Sample format non available" +msgstr "사용할 수 없는 샘플 형ì‹" + +#: aplay/aplay.c:1345 +msgid "Channels count non available" +msgstr "ì±„ë„ ìˆ˜ë¥¼ 사용할 수 ì—†ìŒ" + +#: aplay/aplay.c:1360 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "주ì˜: 정확하지 ì•Šì€ ì†ë„ (ìš”ì²­ë¨ = %iHz, 가져옴 = %iHz)\n" + +#: aplay/aplay.c:1366 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " %s 플러그ì¸ì„ 연결해야 함\n" + +#: aplay/aplay.c:1403 +msgid "Unable to install hw params:" +msgstr "하드웨어 매개변수 설치 불가:" + +#: aplay/aplay.c:1410 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "ë²„í¼ í¬ê¸°ì™€ ê°™ì€ ì£¼ê¸°ëŠ” 사용할 수 ì—†ìŒ (%lu == %lu)" + +#: aplay/aplay.c:1441 +msgid "unable to install sw params:" +msgstr "소프트웨어 매개변수 설치 불가:" + +#: aplay/aplay.c:1476 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "snd_pcm_mmap_begin 문제: %s" + +#: aplay/aplay.c:1505 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "stdin O_NONBLOCK 플래그 설정 실패\n" + +#: aplay/aplay.c:1529 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "\rì¼ì‹œì •ì§€ ëª…ë ¹ì´ ë¬´ì‹œë¨ (하드웨어 ì§€ì› ì—†ìŒ)\n" + +#: aplay/aplay.c:1537 +#, c-format +msgid "pause push error: %s" +msgstr "ì¼ì‹œì •ì§€ 푸시 오류: %s" + +#: aplay/aplay.c:1548 +#, c-format +msgid "pause release error: %s" +msgstr "ì¼ì‹œì •ì§€ 릴리즈 오류: %s" + +#: aplay/aplay.c:1564 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" +"\r=== ì¼ì‹œì •ì§€ " +"=== " + +#: aplay/aplay.c:1606 +#, c-format +msgid "status error: %s" +msgstr "ìƒíƒœ 오류: %s" + +#: aplay/aplay.c:1611 +#, c-format +msgid "fatal %s: %s" +msgstr "치명ì ì¸ %s: %s" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1626 aplay/aplay.c:1634 +msgid "underrun" +msgstr "ì–¸ë”런" + +#: aplay/aplay.c:1612 aplay/aplay.c:1623 aplay/aplay.c:1634 +msgid "overrun" +msgstr "오버런" + +#: aplay/aplay.c:1622 aplay/aplay.c:1633 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (최소 %.3f ms 길ì´)\n" + +#: aplay/aplay.c:1638 +#, c-format +msgid "Status:\n" +msgstr "ìƒíƒœ:\n" + +#: aplay/aplay.c:1642 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "xrun: 준비 오류: %s" + +#: aplay/aplay.c:1648 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "ìƒíƒœ(사용 중):\n" + +#: aplay/aplay.c:1652 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "스트림 í˜•ì‹ ìº¡ì²˜ê°€ 변경ë나요? 복구를 시ë„합니다...\n" + +#: aplay/aplay.c:1654 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "xrun(사용 중): 준비 오류: %s" + +#: aplay/aplay.c:1661 +#, c-format +msgid "Status(R/W):\n" +msgstr "ìƒíƒœ(R/W):\n" + +#: aplay/aplay.c:1664 +#, c-format +msgid "read/write error, state = %s" +msgstr "ì½ê¸°/쓰기 오류, ìƒíƒœ = %s" + +#: aplay/aplay.c:1674 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "중지ë¨. 재개합니다. " + +#: aplay/aplay.c:1679 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "실패. ìŠ¤íŠ¸ë¦¼ì„ ìž¬ê°œí•©ë‹ˆë‹¤. " + +#: aplay/aplay.c:1681 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "중지ë¨: 준비 오류: %s" + +#: aplay/aplay.c:1686 +#, c-format +msgid "Done.\n" +msgstr "완료.\n" + +#: aplay/aplay.c:1708 +#, c-format +msgid " !clip " +msgstr " !í´ë¦½ " + +#: aplay/aplay.c:1855 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "ì§€ì›ë˜ì§€ 않는 비트 í¬ê¸° %d.\n" + +#: aplay/aplay.c:1889 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "최대 í”¼í¬ (%li 샘플): 0x%08x " + +#: aplay/aplay.c:1923 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "ì˜ì‹¬ìŠ¤ëŸ¬ìš´ ë²„í¼ ìœ„ì¹˜ (%li ì „ì²´): 가능 = %li, 지연 = %li, ë²„í¼ = %li\n" + +#: aplay/aplay.c:2051 +#, c-format +msgid "write error: %s" +msgstr "쓰기 오류: %s" + +#: aplay/aplay.c:2099 +#, c-format +msgid "writev error: %s" +msgstr "쓰기 오류: %s" + +#: aplay/aplay.c:2143 +#, c-format +msgid "read error: %s" +msgstr "ì½ê¸° 오류: %s" + +#: aplay/aplay.c:2187 +#, c-format +msgid "readv error: %s" +msgstr "ì½ê¸° 오류: %s" + +#: aplay/aplay.c:2235 +msgid "can't allocate buffer for silence" +msgstr "ë¬´ìŒ ë²„í¼ë¥¼ 할당할 수 ì—†ìŒ" + +#: aplay/aplay.c:2244 aplay/aplay.c:2472 aplay/aplay.c:2477 aplay/aplay.c:2527 +#: aplay/aplay.c:2536 aplay/aplay.c:2543 aplay/aplay.c:2553 aplay/aplay.c:2559 +#: aplay/aplay.c:2631 aplay/aplay.c:2661 aplay/aplay.c:2675 +msgid "write error" +msgstr "쓰기 오류" + +#: aplay/aplay.c:2257 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "voc_pcm_flush - ë¬´ìŒ ì˜¤ë¥˜" + +#: aplay/aplay.c:2260 +msgid "voc_pcm_flush error" +msgstr "voc_pcm_flush 오류" + +#: aplay/aplay.c:2285 +msgid "malloc error" +msgstr "malloc 오류" + +#: aplay/aplay.c:2289 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "í¬ë¦¬ì—ì´í‹°ë¸Œ 랩스 ì±„ë„ íŒŒì¼ '%s' ìž¬ìƒ ì¤‘...\n" + +#: aplay/aplay.c:2357 aplay/aplay.c:2452 +msgid "can't play packed .voc files" +msgstr "ì••ì¶•ëœ .voc 파ì¼ì€ ìž¬ìƒ ë¶ˆê°€" + +#: aplay/aplay.c:2412 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "루프 ìž¬ìƒ ë¶ˆê°€; %s ì°¾ì„ ìˆ˜ ì—†ìŒ\n" + +#: aplay/aplay.c:2461 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "알 수 없는 ë¸”ë¡ ìœ í˜• %d. ë냅니다." + +#: aplay/aplay.c:2595 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "Wave는 %s 형ì‹ì„ ì§€ì›í•˜ì§€ 않습니다..." + +#: aplay/aplay.c:2655 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "ìŠ¤íŒŒí¬ ì˜¤ë””ì˜¤ëŠ” %s 형ì‹ì„ ì§€ì›í•˜ì§€ 않습니다..." + +#: aplay/aplay.c:2736 +msgid "Playing" +msgstr "ìž¬ìƒ ì¤‘" + +#: aplay/aplay.c:2736 +msgid "Recording" +msgstr "ë…¹ìŒ ì¤‘" + +#: aplay/aplay.c:2740 +#, c-format +msgid "Rate %d Hz, " +msgstr "%d Hz ì†ë„, " + +#: aplay/aplay.c:2742 +#, c-format +msgid "Mono" +msgstr "모노" + +#: aplay/aplay.c:2744 +#, c-format +msgid "Stereo" +msgstr "스테레오" + +#: aplay/aplay.c:2746 +#, c-format +msgid "Channels %i" +msgstr "ì±„ë„ %i" + +#: aplay/aplay.c:3344 aplay/aplay.c:3397 +#, c-format +msgid "You need to specify %d files" +msgstr "%d 파ì¼ì„ 지정해야 함" + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - ALSA 시퀀서 ì—°ê²° 매니저\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "Copyright (C) 1999-2000 Takashi Iwai\n" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "사용법:\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * ë‘ í¬íЏ ê°„ì— ì—°ê²°/í•´ì œ\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [-options] sender receiver\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " sender, receiver = í´ë¼ì´ì–¸íЏ:í¬íЏ ì—°ê²°\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect ì—°ê²° í•´ì œ\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive ë‹¨ë… ì—°ê²°\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # ëŒ€ê¸°ì—´ì˜ ì‹¤ì‹œê°„ 스탬프 변환\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # ëŒ€ê¸°ì—´ì˜ í‹±-타임 스팸프 변환\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * ì—°ê²°ëœ í¬íЏ ëª©ë¡ (수신 í–‰ë™ ì—†ìŒ)\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr " aconnect -i|-o [-options]\n" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr " -i,--input ì¸í’‹ (ì½ì„ 수 있는) í¬íЏ 목ë¡\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr " -o,--output 아웃풋 (쓸 수 있는) í¬íЏ\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr " -l,--list ê° í¬íŠ¸ì— í˜„ìž¬ 연결한 목ë¡\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr " * 내보낸 모든 ì—°ê²° 제거\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr " -x, --removeall\n" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "다ìŒìœ¼ë¡œ ì—°ê²° 중" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "다ìŒì— ì—°ê²°ë¨" + +#: seq/aconnect/aconnect.c:171 +#, c-format +msgid "client %d: '%s' [type=%s" +msgstr "í´ë¼ì´ì–¸íЏ %d: '%s' [유형=%s" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "사용ìž" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "커ë„" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "시퀀서를 ì—´ 수 ì—†ìŒ\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "í´ë¼ì´ì–¸íЏ id를 가져올 수 ì—†ìŒ\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "í´ë¼ì´ì–¸íЏ 정보를 설정할 수 ì—†ìŒ\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "ìž˜ëª»ëœ ì „ë‹¬ 주소 %s\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:290 +#, c-format +msgid "invalid destination address %s\n" +msgstr "ìž˜ëª»ëœ ëŒ€ìƒ ì£¼ì†Œ %s\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "ìˆ˜ì‹ ì„ ì°¾ì„ ìˆ˜ ì—†ìŒ\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "ì—°ê²° í•´ì œ 실패 (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "ì´ë¯¸ 수신 ì¤‘ì¸ ì—°ê²°\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "ì—°ê²° 실패 (%s)\n" + +#: seq/aseqnet/aseqnet.c:164 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - ALSA 시퀀서 ë„¤íŠ¸ì›Œí¬ í´ë¼ì´ì–¸íЏ/서버\n" + +#: seq/aseqnet/aseqnet.c:165 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr " Copyright (C) 1999 Takashi Iwai\n" + +#: seq/aseqnet/aseqnet.c:166 +#, c-format +msgid "usage:\n" +msgstr "사용법:\n" + +#: seq/aseqnet/aseqnet.c:167 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " 서버 모드: aseqnet [-options]\n" + +#: seq/aseqnet/aseqnet.c:168 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " í´ë¼ì´ì–¸íЏ 모드: aseqnet [-options] server_host\n" + +#: seq/aseqnet/aseqnet.c:169 +#, c-format +msgid "options:\n" +msgstr "옵션:\n" + +#: seq/aseqnet/aseqnet.c:170 +#, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : TCP í¬íЏ 지정 (서비스 ì´ë¦„ ë˜ëŠ” 숫ìž)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr " -s,--source addr : 주어진 주소(addr)로부터 ì½ìŒ (í´ë¼ì´ì–¸íЏ:í¬íЏ)\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest addr : 주어진 주소(addr)로 쓰기 (í´ë¼ì´ì–¸íЏ:í¬íЏ)\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v, --verbose : ìžì„¸í•œ 메시지 출력\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i, --info : ìˆ˜ì‹ ëœ íŠ¹ì • ì´ë²¤íЏ 출력\n" + +#: seq/aseqnet/aseqnet.c:188 +#, c-format +msgid "can't malloc\n" +msgstr "malloc 불가\n" + +#: seq/aseqnet/aseqnet.c:213 +#, c-format +msgid "closing files..\n" +msgstr "íŒŒì¼ ë‹«ëŠ” 중..\n" + +#: seq/aseqnet/aseqnet.c:272 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "시퀀서 열림: %d:%d\n" + +#: seq/aseqnet/aseqnet.c:279 +#, c-format +msgid "invalid source address %s\n" +msgstr "ìž˜ëª»ëœ ì†ŒìŠ¤ 주소 %s\n" + +#: seq/aseqnet/aseqnet.c:309 +#, c-format +msgid "service '%s' is not found in /etc/services\n" +msgstr "/etc/servicesì—서 서비스 '%s'를 ì°¾ì„ ìˆ˜ ì—†ìŒ \n" + +#: seq/aseqnet/aseqnet.c:377 +#, c-format +msgid "too many connections!\n" +msgstr "ì—°ê²°ì´ ë„ˆë¬´ ë§Žì•„ìš”!\n" + +#: seq/aseqnet/aseqnet.c:388 +#, c-format +msgid "accepted[%d]\n" +msgstr "승ì¸ë¨[%d]\n" + +#: seq/aseqnet/aseqnet.c:411 +#, c-format +msgid "can't get address %s\n" +msgstr "가져올 수 없는 주소 %s\n" + +#: seq/aseqnet/aseqnet.c:422 +#, c-format +msgid "ok.. connected\n" +msgstr "확ì¸.. ì—°ê²°ë¨\n" + +#: seq/aseqnet/aseqnet.c:518 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "ì±„ë„ %2d: 제어 ì´ë²¤íЏ : %5d\n" + +#: seq/aseqnet/aseqnet.c:522 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "ì±„ë„ %2d: í”¼ì¹˜ë°´ë” : %5d\n" + +#: seq/aseqnet/aseqnet.c:526 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "ì±„ë„ %2d: 노트 온 ì´ë²¤íЏ : %5d\n" + +#: seq/aseqnet/aseqnet.c:530 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "ì±„ë„ %2d: 노트 오프 ì´ë²¤íЏ: %5d\n" + +#: seq/aseqnet/aseqnet.c:585 +#, c-format +msgid "disconnected\n" +msgstr "ì—°ê²° í•´ì œë¨\n" + +#: speaker-test/speaker-test.c:121 +msgid "Front Left" +msgstr "왼쪽 ì „ë©´" + +#: speaker-test/speaker-test.c:122 +msgid "Front Right" +msgstr "오른쪽 ì „ë©´" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Left" +msgstr "왼쪽 후면" + +#: speaker-test/speaker-test.c:124 +msgid "Rear Right" +msgstr "오른쪽 후면" + +#: speaker-test/speaker-test.c:126 +msgid "LFE" +msgstr "LFE" + +#: speaker-test/speaker-test.c:127 +msgid "Side Left" +msgstr "왼쪽 측면" + +#: speaker-test/speaker-test.c:128 +msgid "Side Right" +msgstr "오른쪽 측면" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 9" +msgstr "ì±„ë„ 9" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 10" +msgstr "ì±„ë„ 10" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 11" +msgstr "ì±„ë„ 11" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 12" +msgstr "ì±„ë„ 12" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 13" +msgstr "ì±„ë„ 13" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 14" +msgstr "ì±„ë„ 14" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 15" +msgstr "ì±„ë„ 15" + +#: speaker-test/speaker-test.c:136 +msgid "Channel 16" +msgstr "ì±„ë„ 16" + +#: speaker-test/speaker-test.c:519 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "ì†ìƒëœ ìž¬ìƒ êµ¬ì„±: 사용 가능한 구성 ì—†ìŒ: %s\n" + +#: speaker-test/speaker-test.c:526 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "재ìƒì— 사용할 수 없는 액세스 유형: %s\n" + +#: speaker-test/speaker-test.c:533 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "재ìƒì— 사용할 수 없는 샘플 형ì‹: %s\n" + +#: speaker-test/speaker-test.c:540 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "재ìƒì— 사용할 수 없는 ì±„ë„ ìˆ˜(%i): %s\n" + +#: speaker-test/speaker-test.c:548 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "재ìƒì— 사용할 수 없는 %iHz ìƒ˜í”Œë§ ì†ë„: %s\n" + +#: speaker-test/speaker-test.c:553 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "ì†ë„ê°€ ì¼ì¹˜í•˜ì§€ ì•ŠìŒ (%iHz 요청ë¨, %iHz 가져옴, 오류 %d)\n" + +#: speaker-test/speaker-test.c:557 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "ì†ë„를 %iHz로 설정 (%iHz 요청ë¨)\n" + +#: speaker-test/speaker-test.c:563 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "%luì—서 %lu로 보내는 ë²„í¼ í¬ê¸° 범위\n" + +#: speaker-test/speaker-test.c:564 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "%luì—서 %lu로 보내는 주기 í¬ê¸° 범위\n" + +#: speaker-test/speaker-test.c:566 +#, c-format +msgid "Requested period time %u us\n" +msgstr "ìš”ì²­ëœ ì£¼ê¸° 시간 %u us\n" + +#: speaker-test/speaker-test.c:569 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "재ìƒì— %u us 주기 시간 설정 불가: %s\n" + +#: speaker-test/speaker-test.c:575 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "ìš”ì²­ëœ ë²„í¼ ì‹œê°„ %u us\n" + +#: speaker-test/speaker-test.c:578 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "재ìƒì— %u us ë²„í¼ ì‹œê°„ ì ìš© 불가: %s\n" + +#: speaker-test/speaker-test.c:587 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "최대 ë²„í¼ í¬ê¸° %lu 사용함\n" + +#: speaker-test/speaker-test.c:590 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "재ìƒì— ë²„í¼ í¬ê¸° %lu 설정 불가: %s\n" + +#: speaker-test/speaker-test.c:596 +#, c-format +msgid "Periods = %u\n" +msgstr "주기 = %u\n" + +#: speaker-test/speaker-test.c:599 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "재ìƒì— nperiods %u 사용 불가: %s\n" + +#: speaker-test/speaker-test.c:608 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "재ìƒì— 하드웨어 매개변수 사용 불가: %s\n" + +#: speaker-test/speaker-test.c:614 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "ì„¤ì •ëœ period_size = %lu\n" + +#: speaker-test/speaker-test.c:615 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "ì„¤ì •ëœ buffer_size = %lu\n" + +#: speaker-test/speaker-test.c:617 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "버í¼ê°€ 작아 사용할 수 ì—†ìŒ\n" + +#: speaker-test/speaker-test.c:630 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "재ìƒì„ 위한 현재 swparams í™•ì¸ ë¶ˆê°€: %s\n" + +#: speaker-test/speaker-test.c:637 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "" +"재ìƒì„ 위한 시작 임계값 모드 설정 불가: %s\n" +"\n" + +#: speaker-test/speaker-test.c:644 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "재ìƒì„ 위한 최소 íš¨ë ¥ì„ ì„¤ì •í•  수 ì—†ìŒ: %s\n" + +#: speaker-test/speaker-test.c:651 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "재ìƒì„ 위한 소프트웨어 매개변수를 설정할 수 ì—†ìŒ: %s\n" + +#: speaker-test/speaker-test.c:671 +#, c-format +msgid "Unable to set channel map: %s\n" +msgstr "ì±„ë„ ë§µì„ ì„¤ì •í•  수 ì—†ìŒ: %s\n" + +#: speaker-test/speaker-test.c:696 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "ì–¸ë”런ì—서 복구할 수 ì—†ìŒ, 준비 실패: %s\n" + +#: speaker-test/speaker-test.c:707 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "중지 ìƒíƒœì—서 복구할 수 ì—†ìŒ, 준비 실패: %s\n" + +#: speaker-test/speaker-test.c:771 speaker-test/speaker-test.c:1272 +#, c-format +msgid "No enough memory\n" +msgstr "메모리 부족\n" + +#: speaker-test/speaker-test.c:776 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "%s WAV 파ì¼ì„ ì—´ 수 ì—†ìŒ\n" + +#: speaker-test/speaker-test.c:780 speaker-test/speaker-test.c:809 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "ìž˜ëª»ëœ WAV íŒŒì¼ %s\n" + +#: speaker-test/speaker-test.c:785 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "WAV 파ì¼ì´ 아님: %s\n" + +#: speaker-test/speaker-test.c:789 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "%d WAV 형ì‹ì€ %sì— ì§€ì›ë˜ì§€ 않ìŒ\n" + +#: speaker-test/speaker-test.c:794 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s(ì€)는 모노 ìŠ¤íŠ¸ë¦¼ì´ ì•„ë‹˜ (%d 채ë„)\n" + +#: speaker-test/speaker-test.c:799 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "ìƒ˜í”Œë§ ì†ë„ (%d)ê°€ %s와 ì¼ì¹˜í•˜ì§€ ì•ŠìŒ \n" + +#: speaker-test/speaker-test.c:804 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "샘플 í˜•ì‹ ë¹„íŠ¸ %dê°€ %sì— ì§€ì›ë˜ì§€ 않ìŒ\n" + +#: speaker-test/speaker-test.c:865 +#, c-format +msgid "Undefined channel %d\n" +msgstr "ì •ì˜ë˜ì§€ ì•Šì€ ì±„ë„ %d\n" + +#: speaker-test/speaker-test.c:916 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "쓰기 오류: %d,%s\n" + +#: speaker-test/speaker-test.c:918 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery 실패: %d,%s\n" + +#: speaker-test/speaker-test.c:1004 +#, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"사용법: speaker-test [OPTION]... \n" +"-h,--help\të„움ë§\n" +"-D,--device\tìž¬ìƒ ìž¥ì¹˜\n" +"-r,--rate\tHz 스트림 ì†ë„\n" +"-c,--channels\t스트림 ì±„ë„ ìˆ˜\n" +"-f,--frequency\t사ì¸íŒŒ 주파수 Hz\n" +"-F,--format\t샘플 형ì‹\n" +"-b,--buffer\tus 단위 ë§ ë²„í¼ í¬ê¸°\n" +"-p,--period\tus 단위 주기 í¬ê¸°\n" +"-P,--nperiods\t주기 수\n" +"-t,--test\tpink=í•‘í¬ ë…¸ì´ì¦ˆ, sine=사ì¸íŒŒ, wav=WAV 파ì¼\n" +"-l,--nloops\t테스트할 루프 수 지정, 0 = 무한\n" +"-s,--speaker\t싱글 스피커 테스트. 1=왼쪽, 2=오른쪽, 기타\n" +"-w,--wavfile\t주어진 WAV 파ì¼ì„ 테스트 사운드로 사용\n" +"-W,--wavdir\tWAV 파ì¼ì„ í¬í•¨í•˜ëŠ” 디렉토리 지정\n" +"-m,--chmap\t재정ì˜í•  ì±„ë„ ë§µ 지정\n" +"-X,--force-frequency\t30-8000hz 범위 ë°–ì˜ ì£¼íŒŒìˆ˜ ê°•ì œì ìš©\n" +"-S,--scale\tìƒì„±ëœ 테스트 í†¤ì˜ í¬ê¸° 비율 (기본값=80)\n" +"\n" + +#: speaker-test/speaker-test.c:1136 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "유효하지 ì•Šì€ ì£¼ê¸° %d\n" + +#: speaker-test/speaker-test.c:1152 speaker-test/speaker-test.c:1156 +#, c-format +msgid "Invalid test type %s\n" +msgstr "ìž˜ëª»ëœ í…ŒìŠ¤íŠ¸ 유형 %s\n" + +#: speaker-test/speaker-test.c:1189 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "알 수 없는 옵션 '%c'\n" + +#: speaker-test/speaker-test.c:1203 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "-s 옵션 매개변수가 잘못ë습니다.\n" + +#: speaker-test/speaker-test.c:1218 +#, c-format +msgid "Playback device is %s\n" +msgstr "ìž¬ìƒ ìž¥ì¹˜ %s\n" + +#: speaker-test/speaker-test.c:1219 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "스트림 매개변수 %iHz, %s, %i 채ë„\n" + +#: speaker-test/speaker-test.c:1222 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "16 ì˜¥íƒ€ë¸Œì˜ í•‘í¬ ë…¸ì´ì¦ˆ 사용\n" + +#: speaker-test/speaker-test.c:1225 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "%.4fHz 사ì¸(Sine)파 ì†ë„\n" + +#: speaker-test/speaker-test.c:1228 +#, c-format +msgid "WAV file(s)\n" +msgstr "WAV 파ì¼\n" + +#: speaker-test/speaker-test.c:1238 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "ìž¬ìƒ ì—´ê¸° 오류: %d,%s\n" + +#: speaker-test/speaker-test.c:1244 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "매개변수 hwparams 설정 실패: %s\n" + +#: speaker-test/speaker-test.c:1248 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "매개변수 swparams 설정 실패: %s\n" + +#: speaker-test/speaker-test.c:1295 speaker-test/speaker-test.c:1317 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "전송 실패함: %s\n" + +#: speaker-test/speaker-test.c:1303 +#, c-format +msgid "Time per period = %lf\n" +msgstr "주기별 시간 = %lf\n" diff --git a/po/sk.po b/po/sk.po new file mode 100644 index 0000000..213fc39 --- /dev/null +++ b/po/sk.po @@ -0,0 +1,1724 @@ +# Slovak translations for the alsa-utils package. +# Copyright (C) 2009 The ALSA Team +# This file is distributed under the same license as the alsa-utils package. +# Jose Riha , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: alsa-utils 1.0.23\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-10-18 14:55+0200\n" +"PO-Revision-Date: 2020-10-18 15:13+0200\n" +"Last-Translator: Jose Riha \n" +"Language-Team: Slovak\n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.1\n" + +#: alsamixer/card_select.c:77 alsamixer/device_name.c:127 +msgid "Sound Card" +msgstr "Zvuková karta" + +#: alsamixer/card_select.c:115 +msgid "(default)" +msgstr "(predvolená)" + +#: alsamixer/card_select.c:125 +msgid "cannot enumerate sound cards" +msgstr "Chyba poÄas vyÄíslovania zvukových kariet" + +#: alsamixer/card_select.c:147 +msgid "enter device name..." +msgstr "zadajte názov zariadenia..." + +#: alsamixer/cli.c:43 +msgid "Usage: alsamixer [options]" +msgstr "Použitie: alsamixer [možnosti]" + +#: alsamixer/cli.c:44 +msgid "" +"Useful options:\n" +" -h, --help this help\n" +" -c, --card=NUMBER sound card number or id\n" +" -D, --device=NAME mixer device name\n" +" -m, --mouse enable mouse\n" +" -M, --no-mouse disable mouse\n" +" -f, --config=FILE configuration file\n" +" -F, --no-config do not load configuration file\n" +" -V, --view=MODE starting view mode: playback/capture/all" +msgstr "" +"UžitoÄné možnosti:\n" +" -h, --help tento pomocník\n" +" -c, --card=ÄŒÃSLO Äíslo zvukovej karty alebo id\n" +" -D, --device=NÃZOV názov zariadenia mixéra\n" +" -m, --mouse povoliÅ¥ myÅ¡\n" +" -M, --no-mouse vypnúť myÅ¡\n" +" -f, --config=SÚBOR súbor s konfiguráciou\n" +" -F, --no-config nenaÄítaÅ¥ súbor s konfiguráciou\n" +" -V, --view=REŽIM spustiÅ¥ v režime zobrazenia: playback/capture/all (prehrávanie/záznam/vÅ¡etko)" + +#: alsamixer/cli.c:53 +msgid "" +"Debugging options:\n" +" -g, --no-color toggle using of colors\n" +" -a, --abstraction=NAME mixer abstraction level: none/basic" +msgstr "" +"Možnosti pre ladenie:\n" +" -g, --no-color bez farby\n" +" -a, --abstraction=NÃZOV úroveň abstrakcie mixéra: none/basic" + +#: alsamixer/cli.c:88 +#, c-format +msgid "invalid card index: %s\n" +msgstr "neplatné Äíslo karty: %s\n" + +#: alsamixer/cli.c:126 +#, c-format +msgid "unknown abstraction level: %s\n" +msgstr "neznáma úroveň abstrakcie: %s\n" + +#: alsamixer/cli.c:131 +#, c-format +msgid "unknown option: %c\n" +msgstr "neznáma voľba: %c\n" + +#: alsamixer/cli.c:133 +msgid "try `alsamixer --help' for more information\n" +msgstr "skúste `alsamixer --help' pre viac informácií\n" + +#: alsamixer/device_name.c:177 +msgid "Device name:" +msgstr "Názov zariadenia:" + +#: alsamixer/die.c:37 +#, c-format +msgid "%s: %s\n" +msgstr "%s: %s\n" + +#: alsamixer/mixer_display.c:99 +msgid "Card:" +msgstr "Karta:" + +#: alsamixer/mixer_display.c:100 +msgid "Chip:" +msgstr "ÄŒip:" + +#: alsamixer/mixer_display.c:101 +msgid "View:" +msgstr "Pohľad:" + +#: alsamixer/mixer_display.c:102 +msgid "Item:" +msgstr "Položka:" + +#: alsamixer/mixer_display.c:105 +msgid "F1: Help" +msgstr "F1: Pomocník" + +#: alsamixer/mixer_display.c:106 +msgid "F2: System information" +msgstr "F2: Informácie o systéme" + +#: alsamixer/mixer_display.c:107 +msgid "F6: Select sound card" +msgstr "F6: Výber zvukovej karty" + +#: alsamixer/mixer_display.c:108 +msgid "Esc: Exit" +msgstr "Esc: UkonÄiÅ¥" + +#: alsamixer/mixer_display.c:179 +msgid "(unplugged)" +msgstr "(odpojená)" + +#: alsamixer/mixer_display.c:197 +msgid "Playback" +msgstr "Prehrávanie" + +#: alsamixer/mixer_display.c:198 +msgid "Capture" +msgstr "Záznam" + +#: alsamixer/mixer_display.c:199 +msgid "All" +msgstr "VÅ¡etko" + +#: alsamixer/mixer_display.c:240 +msgid "mute" +msgstr "stlmiÅ¥" + +#: alsamixer/mixer_display.c:281 alsamixer/mixer_display.c:291 +msgid "dB gain:" +msgstr "dB zisk:" + +#: alsamixer/mixer_display.c:291 +#, c-format +msgid " [%s %s, %s]" +msgstr " [%s %s; %s]" + +#: alsamixer/mixer_display.c:300 alsamixer/mixer_display.c:306 +#: alsamixer/mixer_display.c:312 alsamixer/mixer_display.c:318 +msgid "Off" +msgstr "Vyp" + +#: alsamixer/mixer_display.c:306 alsamixer/mixer_display.c:318 +msgid "On" +msgstr "Zap" + +#: alsamixer/mixer_display.c:370 +msgid "The sound device was unplugged." +msgstr "Zvukové zariadenie bolo odpojené." + +#: alsamixer/mixer_display.c:371 +msgid "Press F6 to select another sound card." +msgstr "StlaÄte F6 pre výber inej zvukovej karty." + +#: alsamixer/mixer_display.c:386 +msgid "This sound device does not have any playback controls." +msgstr "Toto zvukové zariadenie neposkytuje ovládacie prvky pre prehrávanie." + +#: alsamixer/mixer_display.c:388 +msgid "This sound device does not have any capture controls." +msgstr "Toto zvukové zariadenie neposkytuje ovládacie prvky pre zaznamenávanie." + +#: alsamixer/mixer_display.c:390 +msgid "This sound device does not have any controls." +msgstr "Toto zvukové zariadenie neposkytuje žiadne ovládacie prvky." + +#. TRANSLATORS: playback on; one character +#: alsamixer/mixer_display.c:523 alsamixer/mixer_display.c:528 +msgid "O" +msgstr "O" + +#. TRANSLATORS: playback muted; one character +#: alsamixer/mixer_display.c:525 alsamixer/mixer_display.c:529 +msgid "M" +msgstr "M" + +#. TRANSLATORS: "left"; no more than two characters +#: alsamixer/mixer_display.c:545 +msgid "L" +msgstr "Ľ" + +#. TRANSLATORS: "right"; no more than two characters +#: alsamixer/mixer_display.c:551 +msgid "R" +msgstr "P" + +#. TRANSLATORS: no more than eight characters +#: alsamixer/mixer_display.c:555 +msgid "CAPTURE" +msgstr "ZÃZNAM" + +#: alsamixer/mixer_display.c:611 +msgid "Front" +msgstr "Predné" + +#: alsamixer/mixer_display.c:614 +msgid "Rear" +msgstr "Zadné" + +#: alsamixer/mixer_display.c:617 speaker-test/speaker-test.c:124 +msgid "Center" +msgstr "Stredné" + +#: alsamixer/mixer_display.c:620 +msgid "Woofer" +msgstr "Basy" + +#: alsamixer/mixer_display.c:623 +msgid "Side" +msgstr "BoÄné" + +#: alsamixer/mixer_widget.c:91 alsamixer/mixer_widget.c:96 +msgid "cannot open mixer" +msgstr "Chyba pri otváraní mixéra" + +#: alsamixer/mixer_widget.c:102 alsamixer/mixer_widget.c:179 +msgid "cannot load mixer controls" +msgstr "Chyba pri naÄítavaní ovládacích prvkov mixéra" + +#: alsamixer/mixer_widget.c:169 +#, c-format +msgid "Cannot open mixer device '%s'." +msgstr "Chyba pri otváraní zariadenia mixéra '%s'." + +#: alsamixer/mixer_widget.c:190 +msgid "Esc Exit" +msgstr "Esc UkonÄiÅ¥" + +#: alsamixer/mixer_widget.c:191 +msgid "F1 ? H Help" +msgstr "F1 ? H Pomocník" + +#: alsamixer/mixer_widget.c:192 +msgid "F2 / System information" +msgstr "F2 / Informácie o systéme" + +#: alsamixer/mixer_widget.c:193 +msgid "F3 Show playback controls" +msgstr "F3 ZobraziÅ¥ ovládacie prvky prehrávania" + +#: alsamixer/mixer_widget.c:194 +msgid "F4 Show capture controls" +msgstr "F4 ZobraziÅ¥ ovládacie prvky zaznamenávania" + +#: alsamixer/mixer_widget.c:195 +msgid "F5 Show all controls" +msgstr "F5 ZobraziÅ¥ vÅ¡etky ovládacie prvky" + +#: alsamixer/mixer_widget.c:196 +msgid "Tab Toggle view mode (F3/F4/F5)" +msgstr "Tab Prepnúť režim zobrazenia (F3/F4/F5)" + +#: alsamixer/mixer_widget.c:197 +msgid "F6 S Select sound card" +msgstr "F6 S Výber zvukovej karty" + +#: alsamixer/mixer_widget.c:198 +msgid "L Redraw screen" +msgstr "L AktualizovaÅ¥ obrazovku" + +#: alsamixer/mixer_widget.c:200 +msgid "Left Move to the previous control" +msgstr "Doľava PrejsÅ¥ na predchádzajúci ovládací prvok" + +#: alsamixer/mixer_widget.c:201 +msgid "Right Move to the next control" +msgstr "Doprava PrejsÅ¥ na Äalší ovládací prvok" + +#: alsamixer/mixer_widget.c:203 +msgid "Up/Down Change volume" +msgstr "Hore/Dole Zmena hlasitosti" + +#: alsamixer/mixer_widget.c:204 +msgid "+ - Change volume" +msgstr "+ - Zmena hlasitosti" + +#: alsamixer/mixer_widget.c:205 +msgid "Page Up/Dn Change volume in big steps" +msgstr "PageUp/Down ZmeniÅ¥ hlasitosÅ¥ vo veľkých skokoch" + +#: alsamixer/mixer_widget.c:206 +msgid "End Set volume to 0%" +msgstr "End NastaviÅ¥ hlasitosÅ¥ na 0%" + +#: alsamixer/mixer_widget.c:207 +msgid "0-9 Set volume to 0%-90%" +msgstr "0-9 NastaviÅ¥ hlasitosÅ¥ na 0%-90%" + +#: alsamixer/mixer_widget.c:208 +msgid "Q W E Increase left/both/right volumes" +msgstr "Q W E ZvýšiÅ¥ hlasitosÅ¥ pre ľavý/oba/pravý kanál(y)" + +#. TRANSLATORS: or Y instead of Z +#: alsamixer/mixer_widget.c:210 +msgid "Z X C Decrease left/both/right volumes" +msgstr "Y X C ZnížiÅ¥ hlasitosÅ¥ pre ľavý/oba/pravý kanál(y)" + +#: alsamixer/mixer_widget.c:211 +msgid "B Balance left and right volumes" +msgstr "B SynchronizovaÅ¥ hlasitosÅ¥ ľavého a pravého kanála" + +#: alsamixer/mixer_widget.c:213 +msgid "M Toggle mute" +msgstr "M Prepnúť stlmenie" + +#. TRANSLATORS: or , . +#: alsamixer/mixer_widget.c:215 +msgid "< > Toggle left/right mute" +msgstr ", . Prepnúť stlmenie ľavého/pravého kanála" + +#: alsamixer/mixer_widget.c:217 +msgid "Space Toggle capture" +msgstr "Medzerník Vypnúť/zapnúť zaznamenávanie" + +#. TRANSLATORS: or Insert Delete +#: alsamixer/mixer_widget.c:219 +msgid "; ' Toggle left/right capture" +msgstr "; ' Prepnúť zaznamenávanie ľavého/pravého kanála" + +#: alsamixer/mixer_widget.c:221 +msgid "Authors:" +msgstr "Autori:" + +#: alsamixer/mixer_widget.c:222 +msgid " Tim Janik" +msgstr " Tim Janik" + +#: alsamixer/mixer_widget.c:223 +msgid " Jaroslav Kysela " +msgstr " Jaroslav Kysela " + +#: alsamixer/mixer_widget.c:224 +msgid " Clemens Ladisch " +msgstr " Clemens Ladisch " + +#: alsamixer/mixer_widget.c:226 +msgid "Help" +msgstr "Pomocník" + +#: alsamixer/proc_files.c:56 +msgid "Select File" +msgstr "VybraÅ¥ súbor" + +#: alsamixer/textbox.c:51 alsamixer/textbox.c:65 +msgid "Error" +msgstr "Chyba" + +#: alsamixer/textbox.c:80 +#, c-format +msgid "Cannot open file \"%s\"." +msgstr "Chyba pri otváraní súboru \"%s\"." + +#: aplay/aplay.c:180 +msgid "raw data" +msgstr "surové dáta" + +#: aplay/aplay.c:181 +msgid "VOC" +msgstr "VOC" + +#: aplay/aplay.c:183 +msgid "WAVE" +msgstr "WAVE" + +#: aplay/aplay.c:184 +msgid "Sparc Audio" +msgstr "Sparc-Audio" + +#: aplay/aplay.c:205 +#, c-format +msgid "" +"Usage: %s [OPTION]... [FILE]...\n" +"\n" +"-h, --help help\n" +" --version print current version\n" +"-l, --list-devices list all soundcards and digital audio devices\n" +"-L, --list-pcms list device names\n" +"-D, --device=NAME select PCM by name\n" +"-q, --quiet quiet mode\n" +"-t, --file-type TYPE file type (voc, wav, raw or au)\n" +"-c, --channels=# channels\n" +"-f, --format=FORMAT sample format (case insensitive)\n" +"-r, --rate=# sample rate\n" +"-d, --duration=# interrupt after # seconds\n" +"-s, --samples=# interrupt after # samples per channel\n" +"-M, --mmap mmap stream\n" +"-N, --nonblock nonblocking mode\n" +"-F, --period-time=# distance between interrupts is # microseconds\n" +"-B, --buffer-time=# buffer duration is # microseconds\n" +" --period-size=# distance between interrupts is # frames\n" +" --buffer-size=# buffer duration is # frames\n" +"-A, --avail-min=# min available space for wakeup is # microseconds\n" +"-R, --start-delay=# delay for automatic PCM start is # microseconds \n" +" (relative to buffer size if <= 0)\n" +"-T, --stop-delay=# delay for automatic PCM stop is # microseconds from " +"xrun\n" +"-v, --verbose show PCM structure and setup (accumulative)\n" +"-V, --vumeter=TYPE enable VU meter (TYPE: mono or stereo)\n" +"-I, --separate-channels one file for each channel\n" +"-i, --interactive allow interactive operation from stdin\n" +"-m, --chmap=ch1,ch2,.. Give the channel map to override or follow\n" +" --disable-resample disable automatic rate resample\n" +" --disable-channels disable automatic channel conversions\n" +" --disable-format disable automatic format conversions\n" +" --disable-softvol disable software volume control (softvol)\n" +" --test-position test ring buffer position\n" +" --test-coef=# test coefficient for ring buffer position (default " +"8)\n" +" expression for validation is: coef * (buffer_size / " +"2)\n" +" --test-nowait do not wait for ring buffer - eats whole CPU\n" +" --max-file-time=# start another output file when the old file has " +"recorded\n" +" for this many seconds\n" +" --process-id-file write the process ID here\n" +" --use-strftime apply the strftime facility to the output file " +"name\n" +" --dump-hw-params dump hw_params of the device\n" +" --fatal-errors treat all errors as fatal\n" +msgstr "" +"Syntax: %s [VOĽBA]... [SÚBOR]...\n" +"\n" +"-h, --help pomocník\n" +" --version zobraziÅ¥ aktuálnu verziu\n" +"-l, --list-devices vypísaÅ¥ zoznam vÅ¡etkých zvukových kariet a " +"digitálnych zvukových zariadení na systéme\n" +"-L, --list-pcms vypísaÅ¥ zoznam s názvami zariadení\n" +"-D, --device=NAME vybraÅ¥ zariadenie PCM podľa mena\n" +"-q, --quiet tichý režim\n" +"-t, --file-type TYPE typ súboru (voc, wav, raw alebo au)\n" +"-c, --channels=# kanály\n" +"-f, --format=FORMAT formát vzorkovania (na veľkosti znakov nezáleží)\n" +"-r, --rate=# formát vzorkovania\n" +"-d, --duration=# preruÅ¡iÅ¥ po # sekundách\n" +"-s, --samples=# preruÅ¡iÅ¥ po # vzorkách na kanál\n" +"-M, --mmap mmap prúd\n" +"-N, --nonblock režim bez blokovania\n" +"-F, --period-time=# vzdialenosÅ¥ medzi preruÅ¡eniami je # mikrosekúnd\n" +"-B, --buffer-time=# dĺžka vyrovnávacej pamäte je # mikrosekúnd\n" +" --period-size=# vzdialenosÅ¥ medzi preruÅ¡eniami je # snímkov\n" +" --buffer-size=# dĺžka vyrovnávacej pamäte je # snímkov\n" +"-A, --avail-min=# min. dostupný priestor pre prebudenie (wakeup) je # " +"mikrosekúnd\n" +"-R, --start-delay=# oneskorenie pri automatickom Å¡tarte PCM je # " +"mikrosekúnd \n" +" (relatívne k veľkosti vyrovnávacej pamäte ak <= 0)\n" +"-T, --stop-delay=# oneskorenie pri automatickom zastavení PCM je # " +"mikrosekúnd od xrun\n" +"-v, --verbose zobraziÅ¥ Å¡truktúru a nastavenie PCM (akumulatívne)\n" +"-V, --vumeter=TYPE povoliÅ¥ VU meter (typ: mono alebo stereo)\n" +"-I, --separate-channels jeden súbor pre každý kanál\n" +"-i, --interactive povoliÅ¥ interaktívne ovládanie cez Å¡tandardný vstup " +"(stdin)\n" +"-m, --chmap=ch1,ch2,.. nastaviÅ¥ mapu kanálov, ktorá sa má použiÅ¥ alebo " +"ignorovaÅ¥\n" +" --disable-resample vypnúť automatickú zmenu frekvencie vzorkovania\n" +" --disable-channels vypnúť automatickú konverziu kanálov\n" +" --disable-format vypnúť automatickú konverziu formátu\n" +" --disable-softvol vypnúť softvérovú zmenu hlasitosti (softvol)\n" +" --test-position testovaÅ¥ pozíciu v ring bufferi\n" +" --test-coef=# testovací koeficient pre kontrolu pozície v ring " +"bufferi (predvolené 8)\n" +" vzorec pre kontrolu je: koef * (velkost_buffera / " +"2)\n" +" --test-nowait neÄakaÅ¥ na ring buffer - zhltne celý procesorový " +"Äas\n" +" --max-file-time=# zaÄaÅ¥ zápis do ÄalÅ¡ieho súboru po tom, ako je v " +"aktuálnom súbore zaznamenaných # sekúnd\n" +" --process-id-file zapísaÅ¥ Äíslo procesu do súboru\n" +" --use-strftime formátovaÅ¥ názov súboru funkciou strftime\n" +" --dump-hw-params vypísaÅ¥ hardvérové parametre (hw_params) " +"zariadenia\n" +" --fatal-errors vÅ¡etky chyby vyhodnotiÅ¥ ako závažné\n" + +#: aplay/aplay.c:250 speaker-test/speaker-test.c:945 +#, c-format +msgid "Recognized sample formats are:" +msgstr "Podporované formáty vzorkovania sú:" + +#: aplay/aplay.c:256 +#, c-format +msgid "" +"\n" +"Some of these may not be available on selected hardware\n" +msgstr "" +"\n" +"Niektoré z nich nemusia byÅ¥ dostupné na vybranom hardvéri.\n" + +#: aplay/aplay.c:257 +#, c-format +msgid "The available format shortcuts are:\n" +msgstr "Dostupné skratky formátov sú:\n" + +#: aplay/aplay.c:258 +#, c-format +msgid "-f cd (16 bit little endian, 44100, stereo)\n" +msgstr "-f cd (16 bitov, Little Endian, 44100 Hz, stereo)\n" + +#: aplay/aplay.c:259 +#, c-format +msgid "-f cdr (16 bit big endian, 44100, stereo)\n" +msgstr "-f cdr (16 bitov, Big Endian, 44100 Hz, stereo)\n" + +#: aplay/aplay.c:260 +#, c-format +msgid "-f dat (16 bit little endian, 48000, stereo)\n" +msgstr "-f dat (16 bitov, Little Endian, 48000 Hz, stereo)\n" + +#: aplay/aplay.c:274 +msgid "no soundcards found..." +msgstr "neboli nájdené žiadne zvukové karty..." + +#: aplay/aplay.c:277 +#, c-format +msgid "**** List of %s Hardware Devices ****\n" +msgstr "**** Zoznam hardvérových zariadení (%s) ****\n" + +#: aplay/aplay.c:306 +#, c-format +msgid "card %i: %s [%s], device %i: %s [%s]\n" +msgstr "karta %i: %s [%s], zariadenie %i: %s [%s]\n" + +#: aplay/aplay.c:312 +#, c-format +msgid " Subdevices: %i/%i\n" +msgstr " Podriadené zariadenia: %i/%i\n" + +#: aplay/aplay.c:319 +#, c-format +msgid " Subdevice #%i: %s\n" +msgstr " Podriadené zariadenie #%i: %s\n" + +#: aplay/aplay.c:400 +#, c-format +msgid "Aborted by signal %s...\n" +msgstr "ZruÅ¡ené signálom %s ...\n" + +#: aplay/aplay.c:555 +msgid "command should be named either arecord or aplay" +msgstr "príkaz by sa mal volaÅ¥ buÄ arecord alebo aplay" + +#: aplay/aplay.c:599 +#, c-format +msgid "unrecognized file format %s" +msgstr "nerozpoznaný formát súboru %s" + +#: aplay/aplay.c:606 +#, c-format +msgid "invalid channels argument '%s'" +msgstr "neplatný argument kanála '%s'" + +#: aplay/aplay.c:610 +#, c-format +msgid "value %i for channels is invalid" +msgstr "hodnota %i pre kanále je neplatná" + +#: aplay/aplay.c:629 +#, c-format +msgid "wrong extended format '%s'" +msgstr "neplatný rozšírený formát '%s'" + +#: aplay/aplay.c:637 +#, c-format +msgid "invalid rate argument '%s'" +msgstr "neplatný argument frekvencie '%s'" + +#: aplay/aplay.c:644 +#, c-format +msgid "bad speed value %i" +msgstr "nesprávna hodnota rýchlosti %i" + +#: aplay/aplay.c:650 +msgid "duration and samples arguments cannot be used together" +msgstr "argumenty pre trvanie a poÄet vzoriek nesmú byÅ¥ použité spolu" + +#: aplay/aplay.c:655 +#, c-format +msgid "invalid duration argument '%s'" +msgstr "neplatný argument trvania '%s'" + +#: aplay/aplay.c:662 +msgid "samples and duration arguments cannot be used together" +msgstr "argumenty pre poÄet vzoriek a trvanie nesmú byÅ¥ použité spolu" + +#: aplay/aplay.c:667 +#, c-format +msgid "invalid samples argument '%s'" +msgstr "neplatný argument vzoriek '%s'" + +#: aplay/aplay.c:679 +#, c-format +msgid "invalid period time argument '%s'" +msgstr "neplatný argument Äasu periódy '%s'" + +#: aplay/aplay.c:686 +#, c-format +msgid "invalid buffer time argument '%s'" +msgstr "neplatný argument Äasu vyrovnávacej pamäte '%s'" + +#: aplay/aplay.c:693 +#, c-format +msgid "invalid period size argument '%s'" +msgstr "neplatný argument veľkosti periódy '%s'" + +#: aplay/aplay.c:700 +#, c-format +msgid "invalid buffer size argument '%s'" +msgstr "neplatný argument veľkosti vyrovnávacej pamäte '%s'" + +#: aplay/aplay.c:707 +#, c-format +msgid "invalid min available space argument '%s'" +msgstr "neplatný argument min. dostupného miesta '%s'" + +#: aplay/aplay.c:714 +#, c-format +msgid "invalid start delay argument '%s'" +msgstr "neplatný argument oneskorenia pri Å¡tarte '%s'" + +#: aplay/aplay.c:721 +#, c-format +msgid "invalid stop delay argument '%s'" +msgstr "neplatný argument oneskorenia pri zastavení '%s'" + +#: aplay/aplay.c:776 +#, c-format +msgid "invalid test coef argument '%s'" +msgstr "neplatný argument testovacieho koeficienta '%s'" + +#: aplay/aplay.c:788 +#, c-format +msgid "invalid max file time argument '%s'" +msgstr "neplatný argument pre max. Äas súboru '%s'" + +#: aplay/aplay.c:808 speaker-test/speaker-test.c:571 +#, c-format +msgid "Unable to parse channel map string: %s\n" +msgstr "Chyba pri dekódovaní reÅ¥azca s mapou kanálov: %s\n" + +#: aplay/aplay.c:814 +#, c-format +msgid "Try `%s --help' for more information.\n" +msgstr "Skúste `%s --help' pre ÄalÅ¡ie informácie.\n" + +#: aplay/aplay.c:830 +#, c-format +msgid "audio open error: %s" +msgstr "chyba pri otváraní zariadenia: %s" + +#: aplay/aplay.c:835 +#, c-format +msgid "info error: %s" +msgstr "chyba pri Äítaní informácií o zariadení: %s" + +#: aplay/aplay.c:842 +#, c-format +msgid "nonblock setting error: %s" +msgstr "chyba pri nastavovaní režimu bez blokovania: %s" + +#: aplay/aplay.c:852 aplay/aplay.c:976 aplay/aplay.c:1266 aplay/aplay.c:1466 +#: aplay/aplay.c:1986 aplay/aplay.c:2016 +msgid "not enough memory" +msgstr "nedostatok pamäte" + +#: aplay/aplay.c:876 +#, c-format +msgid "Cannot create process ID file %s: %s" +msgstr "Chyba pri vytváraní súboru s ID procesu %s: %s" + +#: aplay/aplay.c:966 +#, c-format +msgid "read error (called from line %i)" +msgstr "chyba pri Äítaní (volané na riadku %i)" + +#: aplay/aplay.c:1033 +#, c-format +msgid "unknown length of 'fmt ' chunk (read %u, should be %u at least)" +msgstr "neznáma dĺžka 'fmt ' bloku (preÄítané %u, malo by byÅ¥ aspoň %u)" + +#: aplay/aplay.c:1044 +#, c-format +msgid "" +"unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)" +msgstr "" +"neznáma dĺžka rozšíreného 'fmt ' bloku (preÄítané %u, malo by byÅ¥ aspoň %u)" + +#: aplay/aplay.c:1049 +msgid "wrong format tag in extensible 'fmt ' chunk" +msgstr "neznámy formát znaÄky rozšíreného 'fmt ' bloku" + +#: aplay/aplay.c:1056 +#, c-format +msgid "can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded" +msgstr "nie je možné prehraÅ¥ WAV súbor vo formáte 0x%04x, ktorý nie je kódovaný ako PCM alebo FLOAT" + +#: aplay/aplay.c:1061 +#, c-format +msgid "can't play WAVE-files with %d tracks" +msgstr "nie je možné prehraÅ¥ súbory WAV s %d stopami" + +#: aplay/aplay.c:1069 aplay/aplay.c:1195 +#, c-format +msgid "Warning: format is changed to U8\n" +msgstr "Varovanie: formát bol zmenený na U8\n" + +#: aplay/aplay.c:1079 aplay/aplay.c:1092 aplay/aplay.c:1103 +#, c-format +msgid "Warning: format is changed to %s\n" +msgstr "Varovanie: formát bol zmenený na %s\n" + +#: aplay/aplay.c:1108 +#, c-format +msgid "" +" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)" +msgstr "" +" nie je možné prehraÅ¥ súbory WAV s vzorkovaním %d bitov v %d bajtoch (%d " +"kanálov)" + +#: aplay/aplay.c:1131 +#, c-format +msgid " can't play WAVE-files with sample %d bits wide" +msgstr " nie je možné prehraÅ¥ súbory WAV o šírke vzorky %d bitov" + +#: aplay/aplay.c:1189 +#, c-format +msgid "Warning: format is changed to MU_LAW\n" +msgstr "Varovanie: formát bol zmenený na MU_LAW\n" + +#: aplay/aplay.c:1201 +#, c-format +msgid "Warning: format is changed to S16_BE\n" +msgstr "Varovanie: formát bol zmenený na S16_BE\n" + +#: aplay/aplay.c:1214 aplay/aplay.c:2307 aplay/aplay.c:2314 aplay/aplay.c:2839 +msgid "read error" +msgstr "chyba pri Äítaní" + +#: aplay/aplay.c:1244 +msgid "Channel numbers don't match between hw_params and channel map" +msgstr "PoÄet kanálov medzi hw_params a mapou kanálov nesedí" + +#: aplay/aplay.c:1253 +#, c-format +msgid "Warning: unable to get channel map\n" +msgstr "Varovanie: nie je možné získaÅ¥ mapu kanálov\n" + +#: aplay/aplay.c:1287 +#, c-format +msgid "Channel %d doesn't match with hw_params" +msgstr "Kanál %d nesedí s hardvérovými parametrami (hw_params)" + +#: aplay/aplay.c:1314 +msgid "Broken configuration for this PCM: no configurations available" +msgstr "" +"Neplatná konfigurácia pre toto zariadenie PCM: žiadne dostupné konfigurácie" + +#: aplay/aplay.c:1318 +#, c-format +msgid "HW Params of device \"%s\":\n" +msgstr "HW parametre zariadenia \"%s\":\n" + +#: aplay/aplay.c:1338 +msgid "Access type not available" +msgstr "Typ prístupu nie je dostupný" + +#: aplay/aplay.c:1343 +msgid "Sample format non available" +msgstr "Formát vzorkovania nie je dostupný" + +#: aplay/aplay.c:1349 +msgid "Channels count non available" +msgstr "PoÄet kanálov nie je dostupný" + +#: aplay/aplay.c:1364 +#, c-format +msgid "Warning: rate is not accurate (requested = %iHz, got = %iHz)\n" +msgstr "" +"Varovanie: frekvencia je nepresná (vyžiadané: %i Hz, obdržané: %i Hz)\n" + +#: aplay/aplay.c:1370 +#, c-format +msgid " please, try the plug plugin %s\n" +msgstr " vyskúšajte, prosím, doplnok plug: %s\n" + +#: aplay/aplay.c:1407 +msgid "Unable to install hw params:" +msgstr "Chyba pri nastavovaní hw parametrov:" + +#: aplay/aplay.c:1414 +#, c-format +msgid "Can't use period equal to buffer size (%lu == %lu)" +msgstr "" +"Nie je možné použiÅ¥ periódu rovnakú ako veľkosÅ¥ vyrovnávacej pamäte (%lu == " +"%lu)" + +#: aplay/aplay.c:1420 +msgid "Unable to get current sw params." +msgstr "chyba pri nastavovaní softvérových parametrov:" + +#: aplay/aplay.c:1449 +msgid "unable to install sw params:" +msgstr "chyba pri nastavovaní sw parametrov:" + +#: aplay/aplay.c:1484 +#, c-format +msgid "snd_pcm_mmap_begin problem: %s" +msgstr "problém s snd_pcm_mmap_begin: %s" + +#: aplay/aplay.c:1513 +#, c-format +msgid "stdin O_NONBLOCK flag setup failed\n" +msgstr "chyba pri nastavovaní príznaku O_NONBLOCK\n" + +#: aplay/aplay.c:1537 +#, c-format +msgid "\rPAUSE command ignored (no hw support)\n" +msgstr "\rpríkaz PAUSE ignorovaný (chýba podpora v hardvéri)\n" + +#: aplay/aplay.c:1545 +#, c-format +msgid "pause push error: %s" +msgstr "chyba pri pozastavení: %s" + +#: aplay/aplay.c:1556 +#, c-format +msgid "pause release error: %s" +msgstr "chyba pri ukonÄovaní pozastavenia: %s" + +#: aplay/aplay.c:1572 +#, c-format +msgid "" +"\r=== PAUSE === " +msgstr "" +"\r=== PAUZA === " + +#: aplay/aplay.c:1614 +#, c-format +msgid "status error: %s" +msgstr "chyba stavu: %s" + +#: aplay/aplay.c:1619 +#, c-format +msgid "fatal %s: %s" +msgstr "fatálna %s: %s" + +#: aplay/aplay.c:1620 aplay/aplay.c:1631 aplay/aplay.c:1634 aplay/aplay.c:1642 +msgid "underrun" +msgstr "podteÄenie" + +#: aplay/aplay.c:1620 aplay/aplay.c:1631 aplay/aplay.c:1642 +msgid "overrun" +msgstr "preteÄenie" + +#: aplay/aplay.c:1630 aplay/aplay.c:1641 +#, c-format +msgid "%s!!! (at least %.3f ms long)\n" +msgstr "%s!!! (o dĺžke aspoň %.3f ms)\n" + +#: aplay/aplay.c:1646 +#, c-format +msgid "Status:\n" +msgstr "Stav:\n" + +#: aplay/aplay.c:1650 +#, c-format +msgid "xrun: prepare error: %s" +msgstr "xrun: chyba pri inicializácii: %s" + +#: aplay/aplay.c:1656 +#, c-format +msgid "Status(DRAINING):\n" +msgstr "Stav (VYÄŒERPANIE):\n" + +#: aplay/aplay.c:1660 +#, c-format +msgid "capture stream format change? attempting recover...\n" +msgstr "zmena formátu prúdu zaznamenávaných dát? pokúšam sa zotaviÅ¥...\n" + +#: aplay/aplay.c:1662 +#, c-format +msgid "xrun(DRAINING): prepare error: %s" +msgstr "xrun (VYÄŒERPANIE): chyba poÄas inicializácie: %s" + +#: aplay/aplay.c:1669 +#, c-format +msgid "Status(R/W):\n" +msgstr "Stav (R/W):\n" + +#: aplay/aplay.c:1672 +#, c-format +msgid "read/write error, state = %s" +msgstr "chyba Äítania/zápisu, stav = %s" + +#: aplay/aplay.c:1682 +#, c-format +msgid "Suspended. Trying resume. " +msgstr "Pozastavené. Pokúšam sa prebudiÅ¥. " + +#: aplay/aplay.c:1688 +#, c-format +msgid "Failed. Restarting stream. " +msgstr "Zlyhalo. ReÅ¡tartujem stream. " + +#: aplay/aplay.c:1691 +#, c-format +msgid "suspend: prepare error: %s" +msgstr "Pozastavenie: chyba pri inicializácií: %s" + +#: aplay/aplay.c:1696 +#, c-format +msgid "Done.\n" +msgstr "Hotovo.\n" + +#: aplay/aplay.c:1718 +#, c-format +msgid " !clip " +msgstr " !orezanie " + +#: aplay/aplay.c:1862 +#, c-format +msgid "Unsupported bit size %d.\n" +msgstr "Nepodporovaná bitová dĺžka %d.\n" + +#: aplay/aplay.c:1896 +#, c-format +msgid "Max peak (%li samples): 0x%08x " +msgstr "Max. hodnota (%li vzoriek): 0x%08x " + +#: aplay/aplay.c:1930 +#, c-format +msgid "" +"Suspicious buffer position (%li total): avail = %li, delay = %li, buffer = " +"%li\n" +msgstr "" +"Podozrivá pozícia vyrovnávacej pämate (celkom %li): dostupné = %li, " +"oneskorenie = %li, vyr. pamäť = %li\n" + +#: aplay/aplay.c:2058 +#, c-format +msgid "write error: %s" +msgstr "Chyba pri zápise: %s" + +#: aplay/aplay.c:2106 +#, c-format +msgid "writev error: %s" +msgstr "Chyba pri zápise vektora: %s" + +#: aplay/aplay.c:2152 +#, c-format +msgid "read error: %s" +msgstr "Chyba pri Äítaní: %s" + +#: aplay/aplay.c:2199 +#, c-format +msgid "readv error: %s" +msgstr "Chyba pri Äítaní vektora: %s" + +#: aplay/aplay.c:2248 +msgid "can't allocate buffer for silence" +msgstr "Nepodarilo sa alokovaÅ¥ vyrovnávaciu pamäť pre ticho" + +#: aplay/aplay.c:2257 aplay/aplay.c:2485 aplay/aplay.c:2490 aplay/aplay.c:2540 +#: aplay/aplay.c:2549 aplay/aplay.c:2556 aplay/aplay.c:2566 aplay/aplay.c:2572 +#: aplay/aplay.c:2644 aplay/aplay.c:2674 aplay/aplay.c:2688 +msgid "write error" +msgstr "Chyba pri zápise" + +#: aplay/aplay.c:2270 +#, c-format +msgid "voc_pcm_flush - silence error" +msgstr "voc_pcm_flush - chyba pri nastavovaní ticha" + +#: aplay/aplay.c:2273 +msgid "voc_pcm_flush error" +msgstr "chyba voc_pcm_flush" + +#: aplay/aplay.c:2298 +msgid "malloc error" +msgstr "chyba malloc" + +#: aplay/aplay.c:2302 +#, c-format +msgid "Playing Creative Labs Channel file '%s'...\n" +msgstr "Prehrávam súbor Creative Labs Channel '%s'...\n" + +#: aplay/aplay.c:2370 aplay/aplay.c:2465 +msgid "can't play packed .voc files" +msgstr "Nie je možné prehraÅ¥ komprimované súbory .voc" + +#: aplay/aplay.c:2425 +#, c-format +msgid "can't play loops; %s isn't seekable\n" +msgstr "Nedokážem prehraÅ¥ sluÄku; v %s nie je možné skákaÅ¥\n" + +#: aplay/aplay.c:2474 +#, c-format +msgid "unknown blocktype %d. terminate." +msgstr "Neznámy typ bloku %d. KonÄím." + +#: aplay/aplay.c:2608 +#, c-format +msgid "Wave doesn't support %s format..." +msgstr "Wave nepodporuje formát %s..." + +#: aplay/aplay.c:2668 +#, c-format +msgid "Sparc Audio doesn't support %s format..." +msgstr "Sparc audio nepodporuje formát %s..." + +#: aplay/aplay.c:2743 +msgid "Playing" +msgstr "Prehrávam" + +#: aplay/aplay.c:2743 +msgid "Recording" +msgstr "Nahrávam" + +#: aplay/aplay.c:2747 +#, c-format +msgid "Rate %d Hz, " +msgstr "Frekvencia: %d Hz, " + +#: aplay/aplay.c:2749 +#, c-format +msgid "Mono" +msgstr "Mono" + +#: aplay/aplay.c:2751 +#, c-format +msgid "Stereo" +msgstr "Stereo" + +#: aplay/aplay.c:2753 +#, c-format +msgid "Channels %i" +msgstr "%i Kanály" + +#: aplay/aplay.c:3356 +#, c-format +msgid "You need to specify %u files" +msgstr "Musíte zadaÅ¥ %u súborov" + +#: aplay/aplay.c:3409 +#, c-format +msgid "You need to specify %d files" +msgstr "Musíte zadaÅ¥ %d súborov." + +#: seq/aconnect/aconnect.c:49 +#, c-format +msgid "aconnect - ALSA sequencer connection manager\n" +msgstr "aconnect - Správca spojení pre ALSA sekvencér\n" + +#: seq/aconnect/aconnect.c:50 +#, c-format +msgid "Copyright (C) 1999-2000 Takashi Iwai\n" +msgstr "Autorské práva © 1999-2000 Takashi Iwai\n" + +#: seq/aconnect/aconnect.c:51 +#, c-format +msgid "Usage:\n" +msgstr "Syntax:\n" + +#: seq/aconnect/aconnect.c:52 +#, c-format +msgid " * Connection/disconnection between two ports\n" +msgstr " * Vytvorenie/zruÅ¡enie spojenia dvoch prípojok (portov)\n" + +#: seq/aconnect/aconnect.c:53 +#, c-format +msgid " aconnect [-options] sender receiver\n" +msgstr " aconnect [-možnosti] odosielateľ prijímateľ\n" + +#: seq/aconnect/aconnect.c:54 +#, c-format +msgid " sender, receiver = client:port pair\n" +msgstr " odosielateľ, prijímateľ = pár klient:port\n" + +#: seq/aconnect/aconnect.c:55 +#, c-format +msgid " -d,--disconnect disconnect\n" +msgstr " -d,--disconnect zruÅ¡iÅ¥ spojenie\n" + +#: seq/aconnect/aconnect.c:56 +#, c-format +msgid " -e,--exclusive exclusive connection\n" +msgstr " -e,--exclusive výhradné spojenie\n" + +#: seq/aconnect/aconnect.c:57 +#, c-format +msgid " -r,--real # convert real-time-stamp on queue\n" +msgstr " -r,--real # konvertovaÅ¥ príznak reálneho Äasu zdroja\n" + +#: seq/aconnect/aconnect.c:58 +#, c-format +msgid " -t,--tick # convert tick-time-stamp on queue\n" +msgstr " -t,--tick # konvertovaÅ¥ príznak tiknutia Äasu zdroja\n" + +#: seq/aconnect/aconnect.c:59 +#, c-format +msgid " * List connected ports (no subscription action)\n" +msgstr " * ZobraziÅ¥ pripojené prípojky (porty), bez vytvárania spojení\n" + +#: seq/aconnect/aconnect.c:60 +#, c-format +msgid " aconnect -i|-o [-options]\n" +msgstr " aconnect -i|-o [-možnosti]\n" + +#: seq/aconnect/aconnect.c:61 +#, c-format +msgid " -i,--input list input (readable) ports\n" +msgstr "" +" -i,--input zobraziÅ¥ vstupné (Äitateľné) prípojky (porty)\n" + +#: seq/aconnect/aconnect.c:62 +#, c-format +msgid " -o,--output list output (writable) ports\n" +msgstr "" +" -o,--output zobraziÅ¥ výstupné (zapisovateľné) prípojky " +"(porty)\n" + +#: seq/aconnect/aconnect.c:63 +#, c-format +msgid " -l,--list list current connections of each port\n" +msgstr "" +" -l,--list zobraziÅ¥ aktuálne spojenia pri každej prípojke " +"(porte)\n" + +#: seq/aconnect/aconnect.c:64 +#, c-format +msgid " * Remove all exported connections\n" +msgstr " * OdstrániÅ¥ vÅ¡etky exportované spojenia\n" + +#: seq/aconnect/aconnect.c:65 +#, c-format +msgid " -x, --removeall\n" +msgstr " -x,--removeall\n" + +#: seq/aconnect/aconnect.c:132 +msgid "Connecting To" +msgstr "Pripájam k" + +#: seq/aconnect/aconnect.c:133 +msgid "Connected From" +msgstr "Pripojené z" + +#: seq/aconnect/aconnect.c:171 +#, c-format +msgid "client %d: '%s' [type=%s" +msgstr "klient %d: '%s' [typ=%s" + +#: seq/aconnect/aconnect.c:175 +msgid "user" +msgstr "Používateľ" + +#: seq/aconnect/aconnect.c:175 +msgid "kernel" +msgstr "Jadro" + +#: seq/aconnect/aconnect.c:323 +#, c-format +msgid "can't open sequencer\n" +msgstr "Chyba pri otváraní sekvencéra\n" + +#: seq/aconnect/aconnect.c:351 +#, c-format +msgid "can't get client id\n" +msgstr "Chyba pri Äítaní id klienta\n" + +#: seq/aconnect/aconnect.c:358 +#, c-format +msgid "can't set client info\n" +msgstr "Chyba pri nastavovaní informácií o klientovi\n" + +#: seq/aconnect/aconnect.c:365 +#, c-format +msgid "invalid sender address %s\n" +msgstr "Neplatná adresa odosielateľa %s\n" + +#: seq/aconnect/aconnect.c:370 seq/aseqnet/aseqnet.c:290 +#, c-format +msgid "invalid destination address %s\n" +msgstr "Neplatná adresa cieľa %s\n" + +#: seq/aconnect/aconnect.c:384 +#, c-format +msgid "No subscription is found\n" +msgstr "Spojenie sa nenaÅ¡lo\n" + +#: seq/aconnect/aconnect.c:389 +#, c-format +msgid "Disconnection failed (%s)\n" +msgstr "Chyba pri ruÅ¡ení spojenia (%s)\n" + +#: seq/aconnect/aconnect.c:395 +#, c-format +msgid "Connection is already subscribed\n" +msgstr "Spojenie je už zaregistrované\n" + +#: seq/aconnect/aconnect.c:400 +#, c-format +msgid "Connection failed (%s)\n" +msgstr "Chyba spojenia (%s)\n" + +#: seq/aseqnet/aseqnet.c:164 +#, c-format +msgid "aseqnet - network client/server on ALSA sequencer\n" +msgstr "aseqnet - sieÅ¥ový klient/server pre ALSA sekvencér\n" + +#: seq/aseqnet/aseqnet.c:165 +#, c-format +msgid " Copyright (C) 1999 Takashi Iwai\n" +msgstr " Autorské práva © 1999 Takashi Iwai\n" + +#: seq/aseqnet/aseqnet.c:166 +#, c-format +msgid "usage:\n" +msgstr "Syntax:\n" + +#: seq/aseqnet/aseqnet.c:167 +#, c-format +msgid " server mode: aseqnet [-options]\n" +msgstr " Server-Modus: aseqnet [Optionen]\n" + +#: seq/aseqnet/aseqnet.c:168 +#, c-format +msgid " client mode: aseqnet [-options] server_host\n" +msgstr " Client-Modus: aseqnet [Optionen] ServerHost\n" + +#: seq/aseqnet/aseqnet.c:169 +#, c-format +msgid "options:\n" +msgstr "Možnosti:\n" + +#: seq/aseqnet/aseqnet.c:170 +#, c-format +msgid " -p,--port # : specify TCP port (digit or service name)\n" +msgstr " -p,--port # : port TCP (Äíslo alebo meno služby)\n" + +#: seq/aseqnet/aseqnet.c:171 +#, c-format +msgid " -s,--source addr : read from given addr (client:port)\n" +msgstr " -s,--source # : ÄítaÅ¥ z adresy (klient:port)\n" + +#: seq/aseqnet/aseqnet.c:172 +#, c-format +msgid " -d,--dest addr : write to given addr (client:port)\n" +msgstr " -d,--dest # : zapísaÅ¥ na adresu (klient:port)\n" + +#: seq/aseqnet/aseqnet.c:173 +#, c-format +msgid " -v, --verbose : print verbose messages\n" +msgstr " -v,--verbose : zobraziÅ¥ podrobnejÅ¡ie hlásenia\n" + +#: seq/aseqnet/aseqnet.c:174 +#, c-format +msgid " -i, --info : print certain received events\n" +msgstr " -i,--info : zobraziÅ¥ urÄité udalosti príjmu\n" + +#: seq/aseqnet/aseqnet.c:188 +#, c-format +msgid "can't malloc\n" +msgstr "chyba pri malloc\n" + +#: seq/aseqnet/aseqnet.c:213 +#, c-format +msgid "closing files..\n" +msgstr "zatváram súbory ...\n" + +#: seq/aseqnet/aseqnet.c:272 +#, c-format +msgid "sequencer opened: %d:%d\n" +msgstr "sekvencér otvorený: %d:%d\n" + +#: seq/aseqnet/aseqnet.c:279 +#, c-format +msgid "invalid source address %s\n" +msgstr "neplatná zdrojová adresa %s\n" + +#: seq/aseqnet/aseqnet.c:309 +#, c-format +msgid "service '%s' is not found in /etc/services\n" +msgstr "Služba '%s' nebola nájdená v /etc/services\n" + +#: seq/aseqnet/aseqnet.c:377 +#, c-format +msgid "too many connections!\n" +msgstr "PríliÅ¡ veľa spojení!\n" + +#: seq/aseqnet/aseqnet.c:388 +#, c-format +msgid "accepted[%d]\n" +msgstr "prijaté[%d]\n" + +#: seq/aseqnet/aseqnet.c:411 +#, c-format +msgid "can't get address %s\n" +msgstr "nepodarilo sa získaÅ¥ adresu %s\n" + +#: seq/aseqnet/aseqnet.c:422 +#, c-format +msgid "ok.. connected\n" +msgstr "OK ... pripojené\n" + +#: seq/aseqnet/aseqnet.c:518 +#, c-format +msgid "Channel %2d: Control event : %5d\n" +msgstr "Kanál %2d: udalosÅ¥ ovládania: %5d\n" + +#: seq/aseqnet/aseqnet.c:522 +#, c-format +msgid "Channel %2d: Pitchbender : %5d\n" +msgstr "Kanál %2d: Pitchbender : %5d\n" + +#: seq/aseqnet/aseqnet.c:526 +#, c-format +msgid "Channel %2d: Note On event : %5d\n" +msgstr "Kanál %2d: UdalosÅ¥ aktivovanej noty : %5d\n" + +#: seq/aseqnet/aseqnet.c:530 +#, c-format +msgid "Channel %2d: Note Off event: %5d\n" +msgstr "Kanál %2d: UdalosÅ¥ deaktivovanej noty: %5d\n" + +#: seq/aseqnet/aseqnet.c:585 +#, c-format +msgid "disconnected\n" +msgstr "Spojenie zruÅ¡ené\n" + +#: speaker-test/speaker-test.c:120 +msgid "Front Left" +msgstr "Predný ľavý" + +#: speaker-test/speaker-test.c:121 +msgid "Front Right" +msgstr "Predný pravý" + +#: speaker-test/speaker-test.c:122 +msgid "Rear Left" +msgstr "Zadný ľavý" + +#: speaker-test/speaker-test.c:123 +msgid "Rear Right" +msgstr "Zadný pravý" + +#: speaker-test/speaker-test.c:125 +msgid "LFE" +msgstr "Subwoofer" + +#: speaker-test/speaker-test.c:126 +msgid "Side Left" +msgstr "Stredový ľavý" + +#: speaker-test/speaker-test.c:127 +msgid "Side Right" +msgstr "Stredový pravý" + +#: speaker-test/speaker-test.c:128 +msgid "Channel 9" +msgstr "Kanál 9" + +#: speaker-test/speaker-test.c:129 +msgid "Channel 10" +msgstr "Kanál 10" + +#: speaker-test/speaker-test.c:130 +msgid "Channel 11" +msgstr "Kanál 11" + +#: speaker-test/speaker-test.c:131 +msgid "Channel 12" +msgstr "Kanál 12" + +#: speaker-test/speaker-test.c:132 +msgid "Channel 13" +msgstr "Kanál 13" + +#: speaker-test/speaker-test.c:133 +msgid "Channel 14" +msgstr "Kanál 14" + +#: speaker-test/speaker-test.c:134 +msgid "Channel 15" +msgstr "Kanál 15" + +#: speaker-test/speaker-test.c:135 +msgid "Channel 16" +msgstr "Kanál 16" + +#: speaker-test/speaker-test.c:424 +#, c-format +msgid "Broken configuration for playback: no configurations available: %s\n" +msgstr "Neplatná konfigurácia prehrávania: žiadne dostupné konfigurácie: %s\n" + +#: speaker-test/speaker-test.c:431 +#, c-format +msgid "Access type not available for playback: %s\n" +msgstr "Typ prístupu nie je dostupný pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:438 +#, c-format +msgid "Sample format not available for playback: %s\n" +msgstr "Formát vzorkovania nie je dostupný pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:445 +#, c-format +msgid "Channels count (%i) not available for playbacks: %s\n" +msgstr "PoÄet kanálov (%i) nie je dostupný pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:453 +#, c-format +msgid "Rate %iHz not available for playback: %s\n" +msgstr "Frekvencia %i Hz nie je dostupná pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:458 +#, c-format +msgid "Rate doesn't match (requested %iHz, get %iHz, err %d)\n" +msgstr "Frekvencia nesedí (vyžiadané: %i Hz, obdržané: %i Hz, kód chyby %d)\n" + +#: speaker-test/speaker-test.c:462 +#, c-format +msgid "Rate set to %iHz (requested %iHz)\n" +msgstr "Frekvencia nastavená na %i Hz (vyžiadané: %i Hz)\n" + +#: speaker-test/speaker-test.c:468 +#, c-format +msgid "Buffer size range from %lu to %lu\n" +msgstr "VeľkosÅ¥ vyrovnávacej pamäte v rozmedzí od %lu do %lu\n" + +#: speaker-test/speaker-test.c:469 +#, c-format +msgid "Period size range from %lu to %lu\n" +msgstr "VeľkosÅ¥ periódy v rozmedzí od %lu do %lu\n" + +#: speaker-test/speaker-test.c:471 +#, c-format +msgid "Requested period time %u us\n" +msgstr "Vyžiadaný Äas periódy %u µs\n" + +#: speaker-test/speaker-test.c:474 +#, c-format +msgid "Unable to set period time %u us for playback: %s\n" +msgstr "Chyba pri nastavovaní Äasu periódy %u µs pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:480 +#, c-format +msgid "Requested buffer time %u us\n" +msgstr "Vyžiadaný Äas vyrovnávacej pamäte %u µs\n" + +#: speaker-test/speaker-test.c:483 +#, c-format +msgid "Unable to set buffer time %u us for playback: %s\n" +msgstr "" +"Chyba pri nastavovaní Äasu vyrovnávacej pamäte %u µs pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:492 +#, c-format +msgid "Using max buffer size %lu\n" +msgstr "Používam max. veľkosÅ¥ vyrovnávacej pamäte %lu\n" + +#: speaker-test/speaker-test.c:495 +#, c-format +msgid "Unable to set buffer size %lu for playback: %s\n" +msgstr "" +"Chyba pri nastavovaní veľkosti vyrovnávacej pamäte %lu pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:501 +#, c-format +msgid "Periods = %u\n" +msgstr "Periódy = %u\n" + +#: speaker-test/speaker-test.c:504 +#, c-format +msgid "Unable to set nperiods %u for playback: %s\n" +msgstr "Chyba pri nastavovaní poÄtu periód %u pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:513 +#, c-format +msgid "Unable to set hw params for playback: %s\n" +msgstr "Chyba pri nastavovaní hw parametrov pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:519 +#, c-format +msgid "was set period_size = %lu\n" +msgstr "nastavené: period_size = %lu\n" + +#: speaker-test/speaker-test.c:520 +#, c-format +msgid "was set buffer_size = %lu\n" +msgstr "nastavené: buffer_size = %lu\n" + +#: speaker-test/speaker-test.c:522 +#, c-format +msgid "buffer to small, could not use\n" +msgstr "vyrovnávacia pamäť príliÅ¡ malá, nedá sa použiÅ¥\n" + +#: speaker-test/speaker-test.c:535 +#, c-format +msgid "Unable to determine current swparams for playback: %s\n" +msgstr "Chyba pri zisÅ¥ovaní softvérových parametrov pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:542 +#, c-format +msgid "Unable to set start threshold mode for playback: %s\n" +msgstr "Chyba pri nastavovaní zaÄiatku prahového režimu pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:549 +#, c-format +msgid "Unable to set avail min for playback: %s\n" +msgstr "Chyba pri nastavovaní dostupného minima pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:556 +#, c-format +msgid "Unable to set sw params for playback: %s\n" +msgstr "Chyba pri nastavovaní softvérových parametrov pre prehrávanie: %s\n" + +#: speaker-test/speaker-test.c:576 +#, c-format +msgid "Unable to set channel map: %s\n" +msgstr "Chyba pri zostavovaní mapy kanálov: %s\n" + +#: speaker-test/speaker-test.c:601 +#, c-format +msgid "Can't recovery from underrun, prepare failed: %s\n" +msgstr "" +"Nepodarilo sa zotaviÅ¥ z podteÄenia (underrun), chyba pri inicializácii: %s\n" + +#: speaker-test/speaker-test.c:612 +#, c-format +msgid "Can't recovery from suspend, prepare failed: %s\n" +msgstr "Nepodarilo sa zotaviÅ¥ z pozastavenia, chyba pri inicializácii: %s\n" + +#: speaker-test/speaker-test.c:676 speaker-test/speaker-test.c:1191 +#, c-format +msgid "No enough memory\n" +msgstr "Nedostatok pamäte\n" + +#: speaker-test/speaker-test.c:681 +#, c-format +msgid "Cannot open WAV file %s\n" +msgstr "Nepodarilo sa otvoriÅ¥ súbor WAV %s\n" + +#: speaker-test/speaker-test.c:685 speaker-test/speaker-test.c:714 +#, c-format +msgid "Invalid WAV file %s\n" +msgstr "Neplatný súbor WAV %s\n" + +#: speaker-test/speaker-test.c:690 +#, c-format +msgid "Not a WAV file: %s\n" +msgstr "Toto nie je súbor WAV: %s\n" + +#: speaker-test/speaker-test.c:694 +#, c-format +msgid "Unsupported WAV format %d for %s\n" +msgstr "Neplatný formát WAV súboru %d v %s\n" + +#: speaker-test/speaker-test.c:699 +#, c-format +msgid "%s is not a mono stream (%d channels)\n" +msgstr "%s nie je monofónny prúd (%d kanálov)\n" + +#: speaker-test/speaker-test.c:704 +#, c-format +msgid "Sample rate doesn't match (%d) for %s\n" +msgstr "Frekvencia vzorkovania (%d) nesedí s %s\n" + +#: speaker-test/speaker-test.c:709 +#, c-format +msgid "Unsupported sample format bits %d for %s\n" +msgstr "Nepodporovaný formát vzorkovania s %d bitmi v %s\n" + +#: speaker-test/speaker-test.c:770 +#, c-format +msgid "Undefined channel %d\n" +msgstr "Kanál %d nie je definovaný\n" + +#: speaker-test/speaker-test.c:821 +#, c-format +msgid "Write error: %d,%s\n" +msgstr "Chyba pri zápise: %d, %s\n" + +#: speaker-test/speaker-test.c:823 +#, c-format +msgid "xrun_recovery failed: %d,%s\n" +msgstr "xrun_recovery zlyhalo: %d, %s\n" + +#: speaker-test/speaker-test.c:926 +#, c-format +msgid "" +"Usage: speaker-test [OPTION]... \n" +"-h,--help\thelp\n" +"-D,--device\tplayback device\n" +"-r,--rate\tstream rate in Hz\n" +"-c,--channels\tcount of channels in stream\n" +"-f,--frequency\tsine wave frequency in Hz\n" +"-F,--format\tsample format\n" +"-b,--buffer\tring buffer size in us\n" +"-p,--period\tperiod size in us\n" +"-P,--nperiods\tnumber of periods\n" +"-t,--test\tpink=use pink noise, sine=use sine wave, wav=WAV file\n" +"-l,--nloops\tspecify number of loops to test, 0 = infinite\n" +"-s,--speaker\tsingle speaker test. Values 1=Left, 2=right, etc\n" +"-w,--wavfile\tUse the given WAV file as a test sound\n" +"-W,--wavdir\tSpecify the directory containing WAV files\n" +"-m,--chmap\tSpecify the channel map to override\n" +"-X,--force-frequency\tforce frequencies outside the 30-8000hz range\n" +"-S,--scale\tScale of generated test tones in percent (default=80)\n" +"\n" +msgstr "" +"Syntax: speaker-test [VOĽBA]... \n" +"-h,--help\tpomocník\n" +"-D,--device\tzariadenie prehrávania\n" +"-r,--rate\tfrekvencia vzorkovania v Hz\n" +"-c,--channels\tpoÄet kanálov v prúde\n" +"-f,--frequency\tfrekvencia sínusovej vlny v Hz\n" +"-F,--format\tformát vzorkovania\n" +"-b,--buffer\tveľkosÅ¥ ring buffera v µs\n" +"-p,--period\tveľkosÅ¥ periódy v µs\n" +"-P,--nperiods\tpoÄet periód\n" +"-t,--test\tpink=použiÅ¥ ružový Å¡um, sine=použiÅ¥ sínusovú vlnu, wav=súbor " +"WAV\n" +"-l,--nloops\tpoÄet sluÄiek pre test, 0 = nekoneÄno\n" +"-s,--speaker\totestovaÅ¥ samostatný reproduktor. Hodnoty: 1=ľavý, 2=pravý, " +"atÄ.\n" +"-w,--wavfile\tpoužiÅ¥ zadaný WAV súbor pre testovanie\n" +"-W,--wavdir\tpoužiÅ¥ adresár obsahujúci súbory WAV\n" +"-m,--chmap\tmapa kanálov, ktorá bude použitá\n" +"-X,--force-frequency\tvynútiÅ¥ frekvencie mimo rozsahu 30-8000 Hz\n" +"-S,--scale\tÅ¡kálovanie vygenerovaných testovacích tónov v percentách " +"(predvolené=80)\n" +"\n" + +#: speaker-test/speaker-test.c:1058 +#, c-format +msgid "Invalid number of periods %d\n" +msgstr "Neplatný poÄet periód %d\n" + +#: speaker-test/speaker-test.c:1074 speaker-test/speaker-test.c:1078 +#, c-format +msgid "Invalid test type %s\n" +msgstr "Neplatný typ testu %s\n" + +#: speaker-test/speaker-test.c:1111 +#, c-format +msgid "Unknown option '%c'\n" +msgstr "Neznáma voľba '%c'\n" + +#: speaker-test/speaker-test.c:1125 +#, c-format +msgid "Invalid parameter for -s option.\n" +msgstr "Neplatný parameter pre voľbu -s.\n" + +#: speaker-test/speaker-test.c:1140 +#, c-format +msgid "Playback device is %s\n" +msgstr "Zariadenie prehrávania je %s\n" + +#: speaker-test/speaker-test.c:1141 +#, c-format +msgid "Stream parameters are %iHz, %s, %i channels\n" +msgstr "Parametre prúdu sú %i Hz, %s, %i kanály\n" + +#: speaker-test/speaker-test.c:1144 +#, c-format +msgid "Using 16 octaves of pink noise\n" +msgstr "Používam 16 oktáv ružového Å¡umu\n" + +#: speaker-test/speaker-test.c:1147 +#, c-format +msgid "Sine wave rate is %.4fHz\n" +msgstr "Frekvencia sínusovej vlny je %.4f Hz\n" + +#: speaker-test/speaker-test.c:1150 +#, c-format +msgid "WAV file(s)\n" +msgstr "Súbor(y) WAV\n" + +#: speaker-test/speaker-test.c:1160 +#, c-format +msgid "Playback open error: %d,%s\n" +msgstr "Chyba pri otváraní zariadenia prehrávania: %d, %s\n" + +#: speaker-test/speaker-test.c:1166 +#, c-format +msgid "Setting of hwparams failed: %s\n" +msgstr "Chyba pri nastavovaní hardvérových parametrov (hwparams): %s\n" + +#: speaker-test/speaker-test.c:1170 +#, c-format +msgid "Setting of swparams failed: %s\n" +msgstr "Chyba pri nastavovaní softvérových parametrov (swparams): %s\n" + +#: speaker-test/speaker-test.c:1216 speaker-test/speaker-test.c:1238 +#, c-format +msgid "Transfer failed: %s\n" +msgstr "Chyba pri prenose: %s\n" + +#: speaker-test/speaker-test.c:1224 +#, c-format +msgid "Time per period = %lf\n" +msgstr "ÄŒas na periódu = %lf\n" diff --git a/seq/Makefile.am b/seq/Makefile.am new file mode 100644 index 0000000..a617126 --- /dev/null +++ b/seq/Makefile.am @@ -0,0 +1 @@ +SUBDIRS=aconnect aplaymidi aplaymidi2 aseqdump aseqnet aseqsend diff --git a/seq/aconnect/Makefile.am b/seq/aconnect/Makefile.am new file mode 100644 index 0000000..02be5c5 --- /dev/null +++ b/seq/aconnect/Makefile.am @@ -0,0 +1,7 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = README.aconnect aconnect.1 +LDADD = $(LIBINTL) + +bin_PROGRAMS = aconnect +aconnect_SOURCES = aconnect.c +man_MANS = aconnect.1 diff --git a/seq/aconnect/README.aconnect b/seq/aconnect/README.aconnect new file mode 100644 index 0000000..1f6237a --- /dev/null +++ b/seq/aconnect/README.aconnect @@ -0,0 +1,46 @@ +================================================================ + aconnect - control subscriptions + ver.0.1.3 + Copyright (C) 1999-2000 Takashi Iwai +================================================================ + +aconnect is a utility to control subscriptions of two ports as the +third "manager" client. + +For example, the following connects two ports, from 64:0 to 65:0. + + % aconnect 64:0 65:0 + +To disconnect the existing subscription, use -d option. + + % aconnect -d 64:0 65:0 + +To see which port is available as input port, run the following +command: + + % aconnect -i + client 0: 'System' [type=kernel] + 0 'Timer ' + 1 'Announce ' + client 64: '0: MIDI Synth' [type=kernel] + 0 'card 0: synth-midi: 0' + +Similary, to see the output ports, use -o flag. + + % aconnect -o + client 64: '0: MIDI Synth' [type=kernel] + 0 'card 0: synth-midi: 0' + client 65: 'AWE Wave Table Synth : 0' [type=kernel] + 0 'Emu8000 port 0 ' + 1 'Emu8000 port 1 ' + 2 'Emu8000 port 2 ' + 3 'Emu8000 port 3 ' + +The option -l together with -i or -o shows subscribers for each port. + +Ports are connected exclusively when the option -e is specified. + +For modifying time-stamp with a queue, use -r or -t option followed by +a queue index which updates the time-stamp. Former uses real-time queue, +while the latter uses tick queue. The queue must be used (not necessarily +owned) by the receiver client. diff --git a/seq/aconnect/aconnect.1 b/seq/aconnect/aconnect.1 new file mode 100644 index 0000000..5c01519 --- /dev/null +++ b/seq/aconnect/aconnect.1 @@ -0,0 +1,148 @@ +.TH aconnect 1 "August 31, 2000" +.de EX +.nf +.ft CW +.. +.de EE +.ft R +.fi +.. +.SH NAME +aconnect \- ALSA sequencer connection manager + +.SH SYNOPSIS +.B aconnect +[\-d] [\-options] sender receiver +.br +.B aconnect +\-i|\-o [\-options] +.br +.B aconnect +\-x + +.SH DESCRIPTION +.B aconnect +is a utility to connect and disconnect two existing ports on ALSA sequencer +system. +The ports with the arbitrary subscription permission, such as created +by +.B aseqview(1), +can be connected to any (MIDI) device ports using +.B aconnect. +For example, to connect from port 64:0 to 65:0, run as follows: +.IP "" 4 +% aconnect 64:0 65:0 +.PP +The connection is one-way, and the whole data to the sender port (64:0) +is redirected to the receiver port (65:0). When another port (e.g. 65:1) +is attached to the same sender port, the data is sent to both receiver +ports. +For disconnection, use +.B \-d +option. +.sp +.EX +% aconnect \-d 64:0 65:0 +.EE +.PP +The address can be given using the client's name. +.sp +.EX +% aconnect External:0 Emu8000:1 +.EE +.PP +Then the port 0 of the client matching with the string "External" is +connected to the port 1 of the client matching with the "Emu8000". +.PP +Another function of +.B aconnect +is to list the present ports +on the given condition. +The input ports, which may become +.I sender +ports, can be listed with +.B \-i +option. +.sp +.EX +% aconnect \-i +client 0: 'System' [type=kernel] + 0 'Timer ' + 1 'Announce ' +client 64: 'External MIDI\-0' [type=kernel] + 0 'MIDI 0\-0 ' +.EE +.PP +Similarly, to see the output ports, use +.B \-o +flag. +.PP +You can remove all existing exported connections using +.B \-x +option. This function is useful for terminating the ALSA drivers, +because the modules with sequencer connections cannot be unloaded +unless their connections are removed. + +.SH OPTIONS +.SS CONNECTION MANAGEMENT +.TP +.B \-d, \-\-disconnect +Disconnect the given subscription. +.TP +.B \-e, \-\-exclusive +Connect ports with exclusive mode. +Both sender and receiver ports can be no longer connected by any other ports. +.TP +.B \-r, \-\-real queue +Convert time-stamps of event packets to the current value of the given +.I real-time +queue. +This is option is, however, not so useful, since +the receiver port must use (not necessarily own) the specified queue. +.TP +.B \-t, \-\-tick queue +Like +.B \-r +option, but +time-stamps are converted to the current value of the given +.I tick +queue. + +.SS LIST PORTS +.TP +.B \-i, \-\-input +List existing input (readable) ports. +This option is exclusive to +.B \-o. +.TP +.B \-o, \-\-output +List existing output (writable) ports. +This option is exclusive to +.B \-i. +.TP +.B \-a, \-\-all +List inactive ports, too. +.TP +.B \-l, \-\-list +List the current connection status. The connected and connecting ports +from/to each port are listed together. +The suffix flag +.B [ex] +means the connection is exclusive. +The suffix flag +.B [real:#] +and +.B [tick:#] +mean the connection includes real-time and tick conversion on the listed +queue, respectively. + +.SS REMOVE ALL CONNECTIONS +.TP +.B \-x, \-\-removeall +Remove all exported connections. + +.SH "SEE ALSO" +aseqnet(1), aseqview(1) + +.SH AUTHOR +Takashi Iwai diff --git a/seq/aconnect/aconnect.c b/seq/aconnect/aconnect.c new file mode 100644 index 0000000..edd028f --- /dev/null +++ b/seq/aconnect/aconnect.c @@ -0,0 +1,440 @@ +/* + * connect / disconnect two subscriber ports + * ver.0.1.3 + * + * Copyright (C) 1999 Takashi Iwai + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gettext.h" + +static int show_all; + +static void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...) +{ + va_list arg; + + if (err == ENOENT) /* Ignore those misleading "warnings" */ + return; + va_start(arg, fmt); + fprintf(stderr, "ALSA lib %s:%i:(%s) ", file, line, function); + vfprintf(stderr, fmt, arg); + if (err) + fprintf(stderr, ": %s", snd_strerror(err)); + putc('\n', stderr); + va_end(arg); +} + +static void usage(void) +{ + printf(_("aconnect - ALSA sequencer connection manager\n")); + printf(_("Copyright (C) 1999-2000 Takashi Iwai\n")); + printf(_("Usage:\n")); + printf(_(" * Connection/disconnection between two ports\n")); + printf(_(" aconnect [-options] sender receiver\n")); + printf(_(" sender, receiver = client:port pair\n")); + printf(_(" -d,--disconnect disconnect\n")); + printf(_(" -e,--exclusive exclusive connection\n")); + printf(_(" -r,--real # convert real-time-stamp on queue\n")); + printf(_(" -t,--tick # convert tick-time-stamp on queue\n")); + printf(_(" * List connected ports (no subscription action)\n")); + printf(_(" aconnect -i|-o [-options]\n")); + printf(_(" -i,--input list input (readable) ports\n")); + printf(_(" -o,--output list output (writable) ports\n")); + printf(_(" -a,--all show inactive ports, too\n")); + printf(_(" -l,--list list current connections of each port\n")); + printf(_(" * Remove all exported connections\n")); + printf(_(" -x, --removeall\n")); +} + +/* + * check permission (capability) of specified port + */ + +#define LIST_INPUT 1 +#define LIST_OUTPUT 2 + +#define perm_ok(cap,bits) (((cap) & (bits)) == (bits)) + +static int check_direction(snd_seq_port_info_t *pinfo, int bit) +{ + int dir = snd_seq_port_info_get_direction(pinfo); + return !dir || (dir & bit); +} + +static int check_permission(snd_seq_port_info_t *pinfo, int perm) +{ + int cap = snd_seq_port_info_get_capability(pinfo); + + if (cap & SND_SEQ_PORT_CAP_NO_EXPORT) + return 0; + + if (!perm) + return 1; + if (perm & LIST_INPUT) { + if (perm_ok(cap, SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_SUBS_READ) && + check_direction(pinfo, SND_SEQ_PORT_DIR_INPUT)) + return 1; + } + if (perm & LIST_OUTPUT) { + if (perm_ok(cap, SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE) && + check_direction(pinfo, SND_SEQ_PORT_DIR_OUTPUT)) + return 1; + } + return 0; +} + +/* + * list subscribers of specified type + */ +static void list_each_subs(snd_seq_t *seq, snd_seq_query_subscribe_t *subs, int type, const char *msg) +{ + int count = 0; + snd_seq_query_subscribe_set_type(subs, type); + snd_seq_query_subscribe_set_index(subs, 0); + while (snd_seq_query_port_subscribers(seq, subs) >= 0) { + const snd_seq_addr_t *addr; + if (count++ == 0) + printf("\t%s: ", msg); + else + printf(", "); + addr = snd_seq_query_subscribe_get_addr(subs); + printf("%d:%d", addr->client, addr->port); + if (snd_seq_query_subscribe_get_exclusive(subs)) + printf("[ex]"); + if (snd_seq_query_subscribe_get_time_update(subs)) + printf("[%s:%d]", + (snd_seq_query_subscribe_get_time_real(subs) ? "real" : "tick"), + snd_seq_query_subscribe_get_queue(subs)); + snd_seq_query_subscribe_set_index(subs, snd_seq_query_subscribe_get_index(subs) + 1); + } + if (count > 0) + printf("\n"); +} + +/* + * list subscribers + */ +static void list_subscribers(snd_seq_t *seq, const snd_seq_addr_t *addr) +{ + snd_seq_query_subscribe_t *subs; + snd_seq_query_subscribe_alloca(&subs); + snd_seq_query_subscribe_set_root(subs, addr); + list_each_subs(seq, subs, SND_SEQ_QUERY_SUBS_READ, _("Connecting To")); + list_each_subs(seq, subs, SND_SEQ_QUERY_SUBS_WRITE, _("Connected From")); +} + +/* + * search all ports + */ +typedef void (*action_func_t)(snd_seq_t *seq, snd_seq_client_info_t *cinfo, snd_seq_port_info_t *pinfo, int count); + +static void do_search_port(snd_seq_t *seq, int perm, action_func_t do_action) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + int count; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + /* reset query info */ + snd_seq_port_info_set_client(pinfo, snd_seq_client_info_get_client(cinfo)); + snd_seq_port_info_set_port(pinfo, -1); + if (show_all) + snd_seq_port_info_set_capability(pinfo, SND_SEQ_PORT_CAP_INACTIVE); + count = 0; + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + if (check_permission(pinfo, perm)) { + do_action(seq, cinfo, pinfo, count); + count++; + } + if (show_all) + snd_seq_port_info_set_capability(pinfo, SND_SEQ_PORT_CAP_INACTIVE); + } + } +} + + +static void print_port(snd_seq_t *seq ATTRIBUTE_UNUSED, + snd_seq_client_info_t *cinfo, + snd_seq_port_info_t *pinfo, int count) +{ + if (! count) { + int card = -1, pid = -1; + + printf(_("client %d: '%s' [type=%s"), + snd_seq_client_info_get_client(cinfo), + snd_seq_client_info_get_name(cinfo), + (snd_seq_client_info_get_type(cinfo) == SND_SEQ_USER_CLIENT ? + _("user") : _("kernel"))); + switch (snd_seq_client_info_get_midi_version(cinfo)) { + case SND_SEQ_CLIENT_UMP_MIDI_1_0: + printf(",UMP-MIDI1"); + break; + case SND_SEQ_CLIENT_UMP_MIDI_2_0: + printf(",UMP-MIDI2"); + break; + } + card = snd_seq_client_info_get_card(cinfo); + if (card != -1) + printf(",card=%d", card); + + pid = snd_seq_client_info_get_pid(cinfo); + if (pid != -1) + printf(",pid=%d", pid); + printf("]\n"); + } + printf(" %3d '%-16s'", + snd_seq_port_info_get_port(pinfo), + snd_seq_port_info_get_name(pinfo)); + if (snd_seq_port_info_get_capability(pinfo) & SND_SEQ_PORT_CAP_INACTIVE) + printf(" [INACTIVE]"); + printf("\n"); +} + +static void print_port_and_subs(snd_seq_t *seq, snd_seq_client_info_t *cinfo, + snd_seq_port_info_t *pinfo, int count) +{ + print_port(seq, cinfo, pinfo, count); + list_subscribers(seq, snd_seq_port_info_get_addr(pinfo)); +} + + +/* + * remove all (exported) connections + */ +static void remove_connection(snd_seq_t *seq, + snd_seq_client_info_t *info ATTRIBUTE_UNUSED, + snd_seq_port_info_t *pinfo, + int count ATTRIBUTE_UNUSED) +{ + snd_seq_query_subscribe_t *query; + snd_seq_port_info_t *port; + snd_seq_port_subscribe_t *subs; + + snd_seq_query_subscribe_alloca(&query); + snd_seq_query_subscribe_set_root(query, snd_seq_port_info_get_addr(pinfo)); + snd_seq_query_subscribe_set_type(query, SND_SEQ_QUERY_SUBS_READ); + snd_seq_query_subscribe_set_index(query, 0); + + snd_seq_port_info_alloca(&port); + snd_seq_port_subscribe_alloca(&subs); + + while (snd_seq_query_port_subscribers(seq, query) >= 0) { + const snd_seq_addr_t *sender = snd_seq_query_subscribe_get_root(query); + const snd_seq_addr_t *dest = snd_seq_query_subscribe_get_addr(query); + + if (snd_seq_get_any_port_info(seq, dest->client, dest->port, port) < 0 || + !(snd_seq_port_info_get_capability(port) & SND_SEQ_PORT_CAP_SUBS_WRITE) || + (snd_seq_port_info_get_capability(port) & SND_SEQ_PORT_CAP_NO_EXPORT)) { + snd_seq_query_subscribe_set_index(query, snd_seq_query_subscribe_get_index(query) + 1); + continue; + } + snd_seq_port_subscribe_set_queue(subs, snd_seq_query_subscribe_get_queue(query)); + snd_seq_port_subscribe_set_sender(subs, sender); + snd_seq_port_subscribe_set_dest(subs, dest); + if (snd_seq_unsubscribe_port(seq, subs) < 0) { + snd_seq_query_subscribe_set_index(query, snd_seq_query_subscribe_get_index(query) + 1); + } + } +} + +static void remove_all_connections(snd_seq_t *seq) +{ + do_search_port(seq, 0, remove_connection); +} + + +/* + * main.. + */ + +enum { + SUBSCRIBE, UNSUBSCRIBE, LIST, REMOVE_ALL +}; + +#define ACONNECT_OPTS "dior:t:elxa" + +static const struct option long_option[] = { + {"disconnect", 0, NULL, 'd'}, + {"input", 0, NULL, 'i'}, + {"output", 0, NULL, 'o'}, + {"real", 1, NULL, 'r'}, + {"tick", 1, NULL, 't'}, + {"exclusive", 0, NULL, 'e'}, + {"list", 0, NULL, 'l'}, + {"removeall", 0, NULL, 'x'}, + {"all", 0, NULL, 'a'}, + {NULL, 0, NULL, 0}, +}; + +int main(int argc, char **argv) +{ + int c; + snd_seq_t *seq; + int queue = 0, convert_time = 0, convert_real = 0, exclusive = 0; + int command = SUBSCRIBE; + int list_perm = 0; + int client; + int list_subs = 0; + snd_seq_port_subscribe_t *subs; + snd_seq_addr_t sender, dest; + +#ifdef ENABLE_NLS + setlocale(LC_ALL, ""); + textdomain(PACKAGE); +#endif + + while ((c = getopt_long(argc, argv, ACONNECT_OPTS, long_option, NULL)) != -1) { + switch (c) { + case 'd': + command = UNSUBSCRIBE; + break; + case 'i': + command = LIST; + list_perm |= LIST_INPUT; + break; + case 'o': + command = LIST; + list_perm |= LIST_OUTPUT; + break; + case 'e': + exclusive = 1; + break; + case 'r': + queue = atoi(optarg); + convert_time = 1; + convert_real = 1; + break; + case 't': + queue = atoi(optarg); + convert_time = 1; + convert_real = 0; + break; + case 'l': + command = LIST; + list_subs = 1; + break; + case 'x': + command = REMOVE_ALL; + break; + case 'a': + command = LIST; + show_all = 1; + break; + default: + usage(); + exit(1); + } + } + + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) { + fprintf(stderr, _("can't open sequencer\n")); + return 1; + } + + snd_lib_error_set_handler(error_handler); + + switch (command) { + case LIST: + do_search_port(seq, list_perm, + list_subs ? print_port_and_subs : print_port); + snd_seq_close(seq); + return 0; + case REMOVE_ALL: + remove_all_connections(seq); + snd_seq_close(seq); + return 0; + } + + /* connection or disconnection */ + + if (optind + 2 > argc) { + snd_seq_close(seq); + usage(); + exit(1); + } + + if ((client = snd_seq_client_id(seq)) < 0) { + snd_seq_close(seq); + fprintf(stderr, _("can't get client id\n")); + return 1; + } + + /* set client info */ + if (snd_seq_set_client_name(seq, "ALSA Connector") < 0) { + snd_seq_close(seq); + fprintf(stderr, _("can't set client info\n")); + return 1; + } + + /* set subscription */ + if (snd_seq_parse_address(seq, &sender, argv[optind]) < 0) { + snd_seq_close(seq); + fprintf(stderr, _("invalid sender address %s\n"), argv[optind]); + return 1; + } + if (snd_seq_parse_address(seq, &dest, argv[optind + 1]) < 0) { + snd_seq_close(seq); + fprintf(stderr, _("invalid destination address %s\n"), argv[optind + 1]); + return 1; + } + snd_seq_port_subscribe_alloca(&subs); + snd_seq_port_subscribe_set_sender(subs, &sender); + snd_seq_port_subscribe_set_dest(subs, &dest); + snd_seq_port_subscribe_set_queue(subs, queue); + snd_seq_port_subscribe_set_exclusive(subs, exclusive); + snd_seq_port_subscribe_set_time_update(subs, convert_time); + snd_seq_port_subscribe_set_time_real(subs, convert_real); + + if (command == UNSUBSCRIBE) { + if (snd_seq_get_port_subscription(seq, subs) < 0) { + snd_seq_close(seq); + fprintf(stderr, _("No subscription is found\n")); + return 1; + } + if (snd_seq_unsubscribe_port(seq, subs) < 0) { + snd_seq_close(seq); + fprintf(stderr, _("Disconnection failed (%s)\n"), snd_strerror(errno)); + return 1; + } + } else { + if (snd_seq_get_port_subscription(seq, subs) == 0) { + snd_seq_close(seq); + fprintf(stderr, _("Connection is already subscribed\n")); + return 1; + } + if (snd_seq_subscribe_port(seq, subs) < 0) { + snd_seq_close(seq); + fprintf(stderr, _("Connection failed (%s)\n"), snd_strerror(errno)); + return 1; + } + } + + snd_seq_close(seq); + + return 0; +} diff --git a/seq/aplaymidi/Makefile.am b/seq/aplaymidi/Makefile.am new file mode 100644 index 0000000..1fa063e --- /dev/null +++ b/seq/aplaymidi/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = aplaymidi.1 arecordmidi.1 + +bin_PROGRAMS = aplaymidi arecordmidi +man_MANS = aplaymidi.1 arecordmidi.1 diff --git a/seq/aplaymidi/aplaymidi.1 b/seq/aplaymidi/aplaymidi.1 new file mode 100644 index 0000000..0c8238d --- /dev/null +++ b/seq/aplaymidi/aplaymidi.1 @@ -0,0 +1,69 @@ +.TH APLAYMIDI 1 "2 Nov 2011" + +.SH NAME +aplaymidi \- play Standard MIDI Files + +.SH SYNOPSIS +.B aplaymidi +\-p client:port[,...] [\-d delay] midifile ... + +.SH DESCRIPTION +.B aplaymidi +is a command-line utility that plays the specified MIDI file(s) to one +or more ALSA sequencer ports. + +.SH OPTIONS + +.TP +.I \-h, \-\-help +Prints a list of options. + +.TP +.I \-V, \-\-version +Prints the current version. + +.TP +.I \-l, \-\-list +Prints a list of possible output ports. + +.TP +.I \-p, \-\-port=client:port,... +Sets the sequencer port(s) to which the events in the MIDI file(s) are +sent. + +.TP +.I \-u, \-\-ump=version +Changes the sequencer client to the given MIDI version and outputs via +the UMP packets instead of legacy sequencer events. +As of now, the only supported version is 1. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +Multiple ports can be specified to allow playback of MIDI file(s) that +contain events for multiple devices (ports), as specified by "Port +Number" meta events. + +For compatibility with +.B pmidi(1), +the port specification is taken from the +.I ALSA_OUTPUT_PORTS +environment variable if none is given on the command line. + +.TP +.I \-d, \-\-delay=seconds +Specifies how long to wait after the end of each MIDI file, +to allow the last notes to die away. + +.SH BUGS +.B aplaymidi +handles "Port Number" meta events, but not "Port Name" meta events. + +.SH SEE ALSO +pmidi(1) +.br +playmidi(1) + +.SH AUTHOR +Clemens Ladisch diff --git a/seq/aplaymidi/aplaymidi.c b/seq/aplaymidi/aplaymidi.c new file mode 100644 index 0000000..7aeea82 --- /dev/null +++ b/seq/aplaymidi/aplaymidi.c @@ -0,0 +1,1045 @@ +/* + * aplaymidi.c - play Standard MIDI Files to sequencer port(s) + * + * Copyright (c) 2004-2006 Clemens Ladisch + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* TODO: sequencer queue timer selection */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include "version.h" +#include + +/* + * 31.25 kbaud, one start bit, eight data bits, two stop bits. + * (The MIDI spec says one stop bit, but every transmitter uses two, just to be + * sure, so we better not exceed that to avoid overflowing the output buffer.) + */ +#define MIDI_BYTES_PER_SEC (31250 / (1 + 8 + 2)) + +/* + * A MIDI event after being parsed/loaded from the file. + * There could be made a case for using snd_seq_event_t instead. + */ +struct event { + struct event *next; /* linked list */ + + unsigned char type; /* SND_SEQ_EVENT_xxx */ + unsigned char port; /* port index */ + unsigned int tick; + union { + unsigned char d[3]; /* channel and data bytes */ + int tempo; + unsigned int length; /* length of sysex data */ + } data; + unsigned char sysex[0]; +}; + +struct track { + struct event *first_event; /* list of all events in this track */ + int end_tick; /* length of this track */ + + struct event *current_event; /* used while loading and playing */ +}; + +static snd_seq_t *seq; +static int client; +static int port_count; +static snd_seq_addr_t *ports; +static int queue; +static int end_delay = 2; +static const char *file_name; +static FILE *file; +static int file_offset; /* current offset in input file */ +static int num_tracks; +static struct track *tracks; +static int smpte_timing; +static int ump_mode; + +/* prints an error message to stderr */ +static void errormsg(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +/* memory allocation error handling */ +static void check_mem(void *p) +{ + if (!p) + fatal("Out of memory"); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + /* set our name (otherwise it's "Client-xxx") */ + err = snd_seq_set_client_name(seq, "aplaymidi"); + check_snd("set client name", err); + + /* find out who we actually are */ + client = snd_seq_client_id(seq); + check_snd("get client id", client); +} + +/* parses one or more port addresses from the string */ +static void parse_ports(const char *arg) +{ + char *buf, *s, *port_name; + int err; + + /* make a copy of the string because we're going to modify it */ + buf = strdup(arg); + check_mem(buf); + + for (port_name = s = buf; s; port_name = s + 1) { + /* Assume that ports are separated by commas. We don't use + * spaces because those are valid in client names. */ + s = strchr(port_name, ','); + if (s) + *s = '\0'; + + ++port_count; + ports = realloc(ports, port_count * sizeof(snd_seq_addr_t)); + check_mem(ports); + + err = snd_seq_parse_address(seq, &ports[port_count - 1], port_name); + if (err < 0) + fatal("Invalid port %s - %s", port_name, snd_strerror(err)); + } + + free(buf); +} + +static void create_source_port(void) +{ + snd_seq_port_info_t *pinfo; + int err; + + snd_seq_port_info_alloca(&pinfo); + + /* the first created port is 0 anyway, but let's make sure ... */ + snd_seq_port_info_set_port(pinfo, 0); + snd_seq_port_info_set_port_specified(pinfo, 1); + + snd_seq_port_info_set_name(pinfo, "aplaymidi"); + + snd_seq_port_info_set_capability(pinfo, 0); /* sic */ + snd_seq_port_info_set_type(pinfo, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + + err = snd_seq_create_port(seq, pinfo); + check_snd("create port", err); +} + +static void create_queue(void) +{ + queue = snd_seq_alloc_named_queue(seq, "aplaymidi"); + check_snd("create queue", queue); + /* the queue is now locked, which is just fine */ +} + +static void connect_ports(void) +{ + int i, err; + + /* + * We send MIDI events with explicit destination addresses, so we don't + * need any connections to the playback ports. But we connect to those + * anyway to force any underlying RawMIDI ports to remain open while + * we're playing - otherwise, ALSA would reset the port after every + * event. + */ + for (i = 0; i < port_count; ++i) { + err = snd_seq_connect_to(seq, 0, ports[i].client, ports[i].port); + if (err < 0) + fatal("Cannot connect to port %d:%d - %s", + ports[i].client, ports[i].port, snd_strerror(err)); + } +} + +static int read_byte(void) +{ + ++file_offset; + return getc(file); +} + +/* reads a little-endian 32-bit integer */ +static int read_32_le(void) +{ + int value; + value = read_byte(); + value |= read_byte() << 8; + value |= read_byte() << 16; + value |= read_byte() << 24; + return !feof(file) ? value : -1; +} + +/* reads a 4-character identifier */ +static int read_id(void) +{ + return read_32_le(); +} +#define MAKE_ID(c1, c2, c3, c4) ((c1) | ((c2) << 8) | ((c3) << 16) | ((c4) << 24)) + +/* reads a fixed-size big-endian number */ +static int read_int(int bytes) +{ + int c, value = 0; + + do { + c = read_byte(); + if (c == EOF) + return -1; + value = (value << 8) | c; + } while (--bytes); + return value; +} + +/* reads a variable-length number */ +static int read_var(void) +{ + int value, c; + + c = read_byte(); + value = c & 0x7f; + if (c & 0x80) { + c = read_byte(); + value = (value << 7) | (c & 0x7f); + if (c & 0x80) { + c = read_byte(); + value = (value << 7) | (c & 0x7f); + if (c & 0x80) { + c = read_byte(); + value = (value << 7) | c; + if (c & 0x80) + return -1; + } + } + } + return !feof(file) ? value : -1; +} + +/* allocates a new event */ +static struct event *new_event(struct track *track, int sysex_length) +{ + struct event *event; + + event = malloc(sizeof(struct event) + sysex_length); + check_mem(event); + + event->next = NULL; + + /* append at the end of the track's linked list */ + if (track->current_event) + track->current_event->next = event; + else + track->first_event = event; + track->current_event = event; + + return event; +} + +static void skip(int bytes) +{ + while (bytes > 0) + read_byte(), --bytes; +} + +/* reads one complete track from the file */ +static int read_track(struct track *track, int track_end) +{ + int tick = 0; + unsigned char last_cmd = 0; + unsigned char port = 0; + + /* the current file position is after the track ID and length */ + while (file_offset < track_end) { + unsigned char cmd; + struct event *event; + int delta_ticks, len, c; + + delta_ticks = read_var(); + if (delta_ticks < 0) + break; + tick += delta_ticks; + + c = read_byte(); + if (c < 0) + break; + + if (c & 0x80) { + /* have command */ + cmd = c; + if (cmd < 0xf0) + last_cmd = cmd; + } else { + /* running status */ + ungetc(c, file); + file_offset--; + cmd = last_cmd; + if (!cmd) + goto _error; + } + + switch (cmd >> 4) { + /* maps SMF events to ALSA sequencer events */ + static const unsigned char cmd_type[] = { + [0x8] = SND_SEQ_EVENT_NOTEOFF, + [0x9] = SND_SEQ_EVENT_NOTEON, + [0xa] = SND_SEQ_EVENT_KEYPRESS, + [0xb] = SND_SEQ_EVENT_CONTROLLER, + [0xc] = SND_SEQ_EVENT_PGMCHANGE, + [0xd] = SND_SEQ_EVENT_CHANPRESS, + [0xe] = SND_SEQ_EVENT_PITCHBEND + }; + + case 0x8: /* channel msg with 2 parameter bytes */ + case 0x9: + case 0xa: + case 0xb: + case 0xe: + event = new_event(track, 0); + event->type = cmd_type[cmd >> 4]; + event->port = port; + event->tick = tick; + event->data.d[0] = cmd & 0x0f; + event->data.d[1] = read_byte() & 0x7f; + event->data.d[2] = read_byte() & 0x7f; + break; + + case 0xc: /* channel msg with 1 parameter byte */ + case 0xd: + event = new_event(track, 0); + event->type = cmd_type[cmd >> 4]; + event->port = port; + event->tick = tick; + event->data.d[0] = cmd & 0x0f; + event->data.d[1] = read_byte() & 0x7f; + break; + + case 0xf: + switch (cmd) { + case 0xf0: /* sysex */ + case 0xf7: /* continued sysex, or escaped commands */ + len = read_var(); + if (len < 0) + goto _error; + if (cmd == 0xf0) + ++len; + event = new_event(track, len); + event->type = SND_SEQ_EVENT_SYSEX; + event->port = port; + event->tick = tick; + event->data.length = len; + if (cmd == 0xf0) { + event->sysex[0] = 0xf0; + c = 1; + } else { + c = 0; + } + for (; c < len; ++c) + event->sysex[c] = read_byte(); + break; + + case 0xff: /* meta event */ + c = read_byte(); + len = read_var(); + if (len < 0) + goto _error; + + switch (c) { + case 0x21: /* port number */ + if (len < 1) + goto _error; + port = read_byte() % port_count; + skip(len - 1); + break; + + case 0x2f: /* end of track */ + track->end_tick = tick; + skip(track_end - file_offset); + return 1; + + case 0x51: /* tempo */ + if (len < 3) + goto _error; + if (smpte_timing) { + /* SMPTE timing doesn't change */ + skip(len); + } else { + event = new_event(track, 0); + event->type = SND_SEQ_EVENT_TEMPO; + event->port = port; + event->tick = tick; + event->data.tempo = read_byte() << 16; + event->data.tempo |= read_byte() << 8; + event->data.tempo |= read_byte(); + skip(len - 3); + } + break; + + default: /* ignore all other meta events */ + skip(len); + break; + } + break; + + default: /* invalid Fx command */ + goto _error; + } + break; + + default: /* cannot happen */ + goto _error; + } + } +_error: + errormsg("%s: invalid MIDI data (offset %#x)", file_name, file_offset); + return 0; +} + +/* reads an entire MIDI file */ +static int read_smf(void) +{ + int header_len, type, time_division, i, err; + snd_seq_queue_tempo_t *queue_tempo; + + /* the curren position is immediately after the "MThd" id */ + header_len = read_int(4); + if (header_len < 6) { +invalid_format: + errormsg("%s: invalid file format", file_name); + return 0; + } + + type = read_int(2); + if (type != 0 && type != 1) { + errormsg("%s: type %d format is not supported", file_name, type); + return 0; + } + + num_tracks = read_int(2); + if (num_tracks < 1 || num_tracks > 1000) { + errormsg("%s: invalid number of tracks (%d)", file_name, num_tracks); + num_tracks = 0; + return 0; + } + tracks = calloc(num_tracks, sizeof(struct track)); + if (!tracks) { + errormsg("out of memory"); + num_tracks = 0; + return 0; + } + + time_division = read_int(2); + if (time_division < 0) + goto invalid_format; + + /* interpret and set tempo */ + snd_seq_queue_tempo_alloca(&queue_tempo); + smpte_timing = !!(time_division & 0x8000); + if (!smpte_timing) { + /* time_division is ticks per quarter */ + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); /* default: 120 bpm */ + snd_seq_queue_tempo_set_ppq(queue_tempo, time_division); + } else { + /* upper byte is negative frames per second */ + i = 0x80 - ((time_division >> 8) & 0x7f); + /* lower byte is ticks per frame */ + time_division &= 0xff; + /* now pretend that we have quarter-note based timing */ + switch (i) { + case 24: + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 12 * time_division); + break; + case 25: + snd_seq_queue_tempo_set_tempo(queue_tempo, 400000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 10 * time_division); + break; + case 29: /* 30 drop-frame */ + snd_seq_queue_tempo_set_tempo(queue_tempo, 100000000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 2997 * time_division); + break; + case 30: + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 15 * time_division); + break; + default: + errormsg("%s: invalid number of SMPTE frames per second (%d)", + file_name, i); + return 0; + } + } + err = snd_seq_set_queue_tempo(seq, queue, queue_tempo); + if (err < 0) { + errormsg("Cannot set queue tempo (%u/%i)", + snd_seq_queue_tempo_get_tempo(queue_tempo), + snd_seq_queue_tempo_get_ppq(queue_tempo)); + return 0; + } + + /* read tracks */ + for (i = 0; i < num_tracks; ++i) { + int len; + + /* search for MTrk chunk */ + for (;;) { + int id = read_id(); + len = read_int(4); + if (feof(file)) { + errormsg("%s: unexpected end of file", file_name); + return 0; + } + if (len < 0 || len >= 0x10000000) { + errormsg("%s: invalid chunk length %d", file_name, len); + return 0; + } + if (id == MAKE_ID('M', 'T', 'r', 'k')) + break; + skip(len); + } + if (!read_track(&tracks[i], file_offset + len)) + return 0; + } + return 1; +} + +static int read_riff(void) +{ + /* skip file length */ + read_byte(); + read_byte(); + read_byte(); + read_byte(); + + /* check file type ("RMID" = RIFF MIDI) */ + if (read_id() != MAKE_ID('R', 'M', 'I', 'D')) { +invalid_format: + errormsg("%s: invalid file format", file_name); + return 0; + } + /* search for "data" chunk */ + for (;;) { + int id = read_id(); + int len = read_32_le(); + if (feof(file)) { +data_not_found: + errormsg("%s: data chunk not found", file_name); + return 0; + } + if (id == MAKE_ID('d', 'a', 't', 'a')) + break; + if (len < 0) + goto data_not_found; + skip((len + 1) & ~1); + } + /* the "data" chunk must contain data in SMF format */ + if (read_id() != MAKE_ID('M', 'T', 'h', 'd')) + goto invalid_format; + return read_smf(); +} + +static void cleanup_file_data(void) +{ + int i; + struct event *event; + + for (i = 0; i < num_tracks; ++i) { + event = tracks[i].first_event; + while (event) { + struct event *next = event->next; + free(event); + event = next; + } + } + num_tracks = 0; + free(tracks); + tracks = NULL; +} + +static void handle_big_sysex(snd_seq_event_t *ev) +{ + unsigned int length; + ssize_t event_size; + int err; + + length = ev->data.ext.len; + if (length > MIDI_BYTES_PER_SEC) + ev->data.ext.len = MIDI_BYTES_PER_SEC; + event_size = snd_seq_event_length(ev); + if (event_size + 1 > (ssize_t)snd_seq_get_output_buffer_size(seq)) { + err = snd_seq_drain_output(seq); + check_snd("drain output", err); + err = snd_seq_set_output_buffer_size(seq, event_size + 1); + check_snd("set output buffer size", err); + } + while (length > MIDI_BYTES_PER_SEC) { + err = snd_seq_event_output(seq, ev); + check_snd("output event", err); + err = snd_seq_drain_output(seq); + check_snd("drain output", err); + err = snd_seq_sync_output_queue(seq); + check_snd("sync output", err); + if (sleep(1)) + fatal("aborted"); + ev->data.ext.ptr = (char *)ev->data.ext.ptr + MIDI_BYTES_PER_SEC; + length -= MIDI_BYTES_PER_SEC; + } + ev->data.ext.len = length; +} + +static int fill_legacy_event(struct event* event, snd_seq_event_t *ev) +{ + ev->type = event->type; + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: + case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_KEYPRESS: + snd_seq_ev_set_fixed(ev); + ev->data.note.channel = event->data.d[0]; + ev->data.note.note = event->data.d[1]; + ev->data.note.velocity = event->data.d[2]; + break; + case SND_SEQ_EVENT_CONTROLLER: + snd_seq_ev_set_fixed(ev); + ev->data.control.channel = event->data.d[0]; + ev->data.control.param = event->data.d[1]; + ev->data.control.value = event->data.d[2]; + break; + case SND_SEQ_EVENT_PGMCHANGE: + case SND_SEQ_EVENT_CHANPRESS: + snd_seq_ev_set_fixed(ev); + ev->data.control.channel = event->data.d[0]; + ev->data.control.value = event->data.d[1]; + break; + case SND_SEQ_EVENT_PITCHBEND: + snd_seq_ev_set_fixed(ev); + ev->data.control.channel = event->data.d[0]; + ev->data.control.value = ((event->data.d[1]) | + ((event->data.d[2]) << 7)) - 0x2000; + break; + case SND_SEQ_EVENT_SYSEX: + snd_seq_ev_set_variable(ev, event->data.length, event->sysex); + handle_big_sysex(ev); + break; + default: + fatal("Invalid event type %d!", ev->type); + } + return 0; +} + +static unsigned char to_ump_status(unsigned char ev_type) +{ + switch (ev_type) { + case SND_SEQ_EVENT_NOTEON: + return SND_UMP_MSG_NOTE_ON; + case SND_SEQ_EVENT_NOTEOFF: + return SND_UMP_MSG_NOTE_OFF; + case SND_SEQ_EVENT_KEYPRESS: + return SND_UMP_MSG_POLY_PRESSURE; + case SND_SEQ_EVENT_CONTROLLER: + return SND_UMP_MSG_CONTROL_CHANGE; + case SND_SEQ_EVENT_PGMCHANGE: + return SND_UMP_MSG_PROGRAM_CHANGE; + case SND_SEQ_EVENT_CHANPRESS: + return SND_UMP_MSG_CHANNEL_PRESSURE; + case SND_SEQ_EVENT_PITCHBEND: + return SND_UMP_MSG_PITCHBEND; + default: + return 0; + } +} + +static int fill_ump_event(struct event* event, snd_seq_ump_event_t *ump_ev, + const snd_seq_event_t *ev) +{ + snd_ump_msg_midi1_t ump = {}; + unsigned char status = to_ump_status(event->type); + + memcpy(ump_ev, ev, sizeof(*ev)); + if (!status) + return 0; /* handle as is */ + + ump.note_on.type = SND_UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE; + switch (event->type) { + case SND_SEQ_EVENT_NOTEON: + /* correct the note-on with velocity 0 to note-off; + * UMP may handle velocity 0 differently + */ + if (!ev->data.note.velocity) + status = SND_UMP_MSG_NOTE_OFF; + /* fallthrough */ + case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_KEYPRESS: + ump.note_on.status = status; + ump.note_on.channel = event->data.d[0]; + ump.note_on.note = event->data.d[1]; + ump.note_on.velocity = event->data.d[2]; + break; + case SND_SEQ_EVENT_CONTROLLER: + ump.control_change.status = status; + ump.control_change.channel = event->data.d[0]; + ump.control_change.index = event->data.d[1]; + ump.control_change.data = event->data.d[2]; + break; + case SND_SEQ_EVENT_PGMCHANGE: + ump.program_change.status = status; + ump.program_change.channel = event->data.d[0]; + ump.program_change.program = event->data.d[1]; + break; + case SND_SEQ_EVENT_CHANPRESS: + ump.channel_pressure.status = status; + ump.channel_pressure.channel = event->data.d[0]; + ump.channel_pressure.data = event->data.d[1]; + break; + case SND_SEQ_EVENT_PITCHBEND: + ump.pitchbend.status = status; + ump.pitchbend.channel = event->data.d[0]; + ump.pitchbend.data_msb = event->data.d[2]; + ump.pitchbend.data_lsb = event->data.d[1]; + break; + default: + return 0; /* handle as is */ + } + snd_seq_ev_set_ump_data(ump_ev, &ump, sizeof(ump)); + return 0; +} + +static void play_midi(void) +{ + snd_seq_ump_event_t ump_ev; + snd_seq_event_t ev; + int i, max_tick, err; + + /* calculate length of the entire file */ + max_tick = -1; + for (i = 0; i < num_tracks; ++i) { + if (tracks[i].end_tick > max_tick) + max_tick = tracks[i].end_tick; + } + + /* initialize current position in each track */ + for (i = 0; i < num_tracks; ++i) + tracks[i].current_event = tracks[i].first_event; + + /* common settings for all our events */ + snd_seq_ev_clear(&ev); + ev.queue = queue; + ev.source.port = 0; + ev.flags = SND_SEQ_TIME_STAMP_TICK; + + err = snd_seq_start_queue(seq, queue, NULL); + check_snd("start queue", err); + /* The queue won't be started until the START_QUEUE event is + * actually drained to the kernel, which is exactly what we want. */ + + for (;;) { + struct event* event = NULL; + struct track* event_track = NULL; + int i, min_tick = max_tick + 1; + + /* search next event */ + for (i = 0; i < num_tracks; ++i) { + struct track *track = &tracks[i]; + struct event *e2 = track->current_event; + if (e2 && e2->tick < (unsigned int)min_tick) { + min_tick = e2->tick; + event = e2; + event_track = track; + } + } + if (!event) + break; /* end of song reached */ + + /* advance pointer to next event */ + event_track->current_event = event->next; + + /* output the event */ + ev.time.tick = event->tick; + ev.dest = ports[event->port]; + if (event->type == SND_SEQ_EVENT_TEMPO) { + snd_seq_ev_set_fixed(&ev); + ev.type = event->type; + ev.dest.client = SND_SEQ_CLIENT_SYSTEM; + ev.dest.port = SND_SEQ_PORT_SYSTEM_TIMER; + ev.data.queue.queue = queue; + ev.data.queue.param.value = event->data.tempo; + } else { + err = fill_legacy_event(event, &ev); + if (err < 0) + continue; + } + + if (ump_mode) { + err = fill_ump_event(event, &ump_ev, &ev); + if (err < 0) + continue; + err = snd_seq_ump_event_output(seq, &ump_ev); + check_snd("output event", err); + continue; + } + + /* this blocks when the output pool has been filled */ + err = snd_seq_event_output(seq, &ev); + check_snd("output event", err); + } + + /* schedule queue stop at end of song */ + snd_seq_ev_set_fixed(&ev); + ev.type = SND_SEQ_EVENT_STOP; + ev.time.tick = max_tick; + ev.dest.client = SND_SEQ_CLIENT_SYSTEM; + ev.dest.port = SND_SEQ_PORT_SYSTEM_TIMER; + ev.data.queue.queue = queue; + err = snd_seq_event_output(seq, &ev); + check_snd("output event", err); + + /* make sure that the sequencer sees all our events */ + err = snd_seq_drain_output(seq); + check_snd("drain output", err); + + /* + * There are three possibilities how to wait until all events have + * been played: + * 1) send an event back to us (like pmidi does), and wait for it; + * 2) wait for the EVENT_STOP notification for our queue which is sent + * by the system timer port (this would require a subscription); + * 3) wait until the output pool is empty. + * The last is the simplest. + */ + err = snd_seq_sync_output_queue(seq); + check_snd("sync output", err); + + /* give the last notes time to die away */ + if (end_delay > 0) + sleep(end_delay); +} + +static void play_file(void) +{ + int ok; + + if (!strcmp(file_name, "-")) + file = stdin; + else + file = fopen(file_name, "rb"); + if (!file) { + errormsg("Cannot open %s - %s", file_name, strerror(errno)); + return; + } + + file_offset = 0; + ok = 0; + + switch (read_id()) { + case MAKE_ID('M', 'T', 'h', 'd'): + ok = read_smf(); + break; + case MAKE_ID('R', 'I', 'F', 'F'): + ok = read_riff(); + break; + default: + errormsg("%s is not a Standard MIDI File", file_name); + break; + } + + if (file != stdin) + fclose(file); + + if (ok) + play_midi(); + + cleanup_file_data(); +} + +static void list_ports(void) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + puts(" Port Client name Port name"); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + /* port must understand MIDI messages */ + if (!(snd_seq_port_info_get_type(pinfo) + & SND_SEQ_PORT_TYPE_MIDI_GENERIC)) + continue; + /* we need both WRITE and SUBS_WRITE */ + if ((snd_seq_port_info_get_capability(pinfo) + & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)) + != (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)) + continue; + printf("%3d:%-3d %-32.32s %s\n", + snd_seq_port_info_get_client(pinfo), + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } +} + +static void usage(const char *argv0) +{ + printf( + "Usage: %s -p client:port[,...] [-d delay] midifile ...\n" + "-h, --help this help\n" + "-V, --version print current version\n" + "-l, --list list all possible output ports\n" + "-p, --port=client:port,... set port(s) to play to\n" + "-u, --ump=version UMP output (only version=1 is supported)\n" + "-d, --delay=seconds delay after song ends\n", + argv0); +} + +static void version(void) +{ + puts("aplaymidi version " SND_UTIL_VERSION_STR); +} + +#define OPTIONS "hVlp:d:u:" + +int main(int argc, char *argv[]) +{ + static const char short_options[] = OPTIONS; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"list", 0, NULL, 'l'}, + {"port", 1, NULL, 'p'}, + {"ump", 1, NULL, 'u'}, + {"delay", 1, NULL, 'd'}, + {0} + }; + int c; + int do_list = 0; + + init_seq(); + + while ((c = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'l': + do_list = 1; + break; + case 'p': + parse_ports(optarg); + break; + case 'd': + end_delay = atoi(optarg); + break; + case 'u': + ump_mode = atoi(optarg); + if (ump_mode < 0 || ump_mode > 1) + fatal("Only MIDI 1.0 is supported"); + break; + default: + usage(argv[0]); + return 1; + } + } + + + if (ump_mode) { + int err; + err = snd_seq_set_client_midi_version(seq, SND_SEQ_CLIENT_UMP_MIDI_1_0); + check_snd("set midi version", err); + } + + if (do_list) { + list_ports(); + } else { + if (port_count < 1) { + /* use env var for compatibility with pmidi */ + const char *ports_str = getenv("ALSA_OUTPUT_PORTS"); + if (ports_str) + parse_ports(ports_str); + if (port_count < 1) { + errormsg("Please specify at least one port with --port."); + return 1; + } + } + if (optind >= argc) { + errormsg("Please specify a file to play."); + return 1; + } + + create_source_port(); + create_queue(); + connect_ports(); + + for (; optind < argc; ++optind) { + file_name = argv[optind]; + play_file(); + } + } + snd_seq_close(seq); + return 0; +} diff --git a/seq/aplaymidi/arecordmidi.1 b/seq/aplaymidi/arecordmidi.1 new file mode 100644 index 0000000..585f2fc --- /dev/null +++ b/seq/aplaymidi/arecordmidi.1 @@ -0,0 +1,82 @@ +.TH ARECORDMIDI 1 "17 Sep 2007" + +.SH NAME +arecordmidi \- record Standard MIDI Files + +.SH SYNOPSIS +.B arecordmidi +\-p client:port[,...] [options] midifile + +.SH DESCRIPTION +.B arecordmidi +is a command-line utility that records a Standard MIDI File from one or +more ALSA sequencer ports. + +To stop recording, press Ctrl+C. + +.SH OPTIONS + +.TP +.I \-h,\-\-help +Prints a list of options. + +.TP +.I \-V,\-\-version +Prints the current version. + +.TP +.I \-l,\-\-list +Prints a list of possible input ports. + +.TP +.I \-p,\-\-port=client:port,... +Sets the sequencer port(s) from which events are recorded. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +.TP +.I \-b,\-\-bpm=beats +Sets the musical tempo of the MIDI file, in beats per minute. +The default value is 120 BPM. + +.TP +.I \-f,\-\-fps=frames +Sets the SMPTE resolution, in frames per second. +Possible values are 24, 25, 29.97 (for 30 drop-frame), and 30. + +.TP +.I \-t,\-\-ticks=ticks +Sets the resolution of timestamps (ticks) in the MIDI file, +in ticks per beat (when using musical tempo) or ticks per frame +(when using SMPTE timing). +The default value is 384 ticks/beat or 40 ticks/frame, respectively. + +.TP +.I \-s,\-\-split\-channels +Specifies that the data for each MIDI channel should be written to a +separate track in the MIDI file. +This will result in a "format 1" file. +Otherwise, when there is only one track, +.B arecordmidi +will generate a "format 0" file. + +.TP +.I \-m,\-\-metronome=client:port +Plays a metronome signal on the specified sequencer port. + +Metronome sounds are played on channel 10, MIDI notes 33 & 34 (GM2/GS/XG +metronome standard notes), with velocity 100 and duration 1. + +.TP +.I \-i,\-\-timesig=numerator:denominator +Sets the time signature for the MIDI file and metronome. + +The time signature is specified as usual with two numbers, representing +the numerator and denominator of the time signature as it would be +notated. The denominator must be a power of two. Both numbers should be +separated by a colon. The time signature is 4:4 by default. + +.SH AUTHOR +Clemens Ladisch diff --git a/seq/aplaymidi/arecordmidi.c b/seq/aplaymidi/arecordmidi.c new file mode 100644 index 0000000..a4c1181 --- /dev/null +++ b/seq/aplaymidi/arecordmidi.c @@ -0,0 +1,921 @@ +/* + * arecordmidi.c - record standard MIDI files from sequencer ports + * + * Copyright (c) 2004-2005 Clemens Ladisch + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* TODO: sequencer queue timer selection */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "aconfig.h" +#include "version.h" + +#define BUFFER_SIZE 4088 + +/* linked list of buffers, stores data as in the .mid file */ +struct buffer { + struct buffer *next; + unsigned char buf[BUFFER_SIZE]; +}; + +struct smf_track { + int size; /* size of entire data */ + int cur_buf_size; /* size of cur_buf */ + struct buffer *cur_buf; + snd_seq_tick_time_t last_tick; /* end of track */ + unsigned char last_command; /* used for running status */ + int used; /* anything record on this track */ + struct buffer first_buf; /* list head */ +}; + +/* timing/sysex + 16 channels */ +#define TRACKS_PER_PORT 17 + +/* metronome settings */ +/* TODO: create options for this */ +#define METRONOME_CHANNEL 9 +#define METRONOME_STRONG_NOTE 34 +#define METRONOME_WEAK_NOTE 33 +#define METRONOME_VELOCITY 100 +#define METRONOME_PROGRAM 0 + +static snd_seq_t *seq; +static int client; +static int port_count; +static snd_seq_addr_t *ports; +static int queue; +static int smpte_timing = 0; +static int beats = 120; +static int frames; +static int ticks = 0; +static FILE *file; +static int channel_split; +static int num_tracks; +static struct smf_track *tracks; +static volatile sig_atomic_t stop = 0; +static int use_metronome = 0; +static snd_seq_addr_t metronome_port; +static int metronome_weak_note = METRONOME_WEAK_NOTE; +static int metronome_strong_note = METRONOME_STRONG_NOTE; +static int metronome_velocity = METRONOME_VELOCITY; +static int metronome_program = METRONOME_PROGRAM; +static int metronome_channel = METRONOME_CHANNEL; +static int ts_num = 4; /* time signature: numerator */ +static int ts_div = 4; /* time signature: denominator */ +static int ts_dd = 2; /* time signature: denominator as a power of two */ + +/* Parse a decimal number from a command line argument. */ +static long arg_parse_decimal_num(const char *str, int *err) +{ + long val; + char *endptr; + + errno = 0; + val = strtol(str, &endptr, 0); + if (errno > 0) { + *err = -errno; + return 0; + } + if (*endptr != '\0') { + *err = -EINVAL; + return 0; + } + + return val; +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +/* memory allocation error handling */ +static void check_mem(void *p) +{ + if (!p) + fatal("Out of memory"); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + /* find out our client's id */ + client = snd_seq_client_id(seq); + check_snd("get client id", client); + + /* set our client's name */ + err = snd_seq_set_client_name(seq, "arecordmidi"); + check_snd("set client name", err); +} + +/* parses one or more port addresses from the string */ +static void parse_ports(const char *arg) +{ + char *buf, *s, *port_name; + int err; + + /* make a copy of the string because we're going to modify it */ + buf = strdup(arg); + check_mem(buf); + + for (port_name = s = buf; s; port_name = s + 1) { + /* Assume that ports are separated by commas. We don't use + * spaces because those are valid in client names. */ + s = strchr(port_name, ','); + if (s) + *s = '\0'; + + ++port_count; + ports = realloc(ports, port_count * sizeof(snd_seq_addr_t)); + check_mem(ports); + + err = snd_seq_parse_address(seq, &ports[port_count - 1], port_name); + if (err < 0) + fatal("Invalid port %s - %s", port_name, snd_strerror(err)); + } + + free(buf); +} + +/* parses the metronome port address */ +static void init_metronome(const char *arg) +{ + int err; + + err = snd_seq_parse_address(seq, &metronome_port, arg); + if (err < 0) + fatal("Invalid port %s - %s", arg, snd_strerror(err)); + use_metronome = 1; +} + +/* parses time signature specification */ +static void time_signature(const char *arg) +{ + long x = 0; + char *sep; + + x = strtol(arg, &sep, 10); + if (x < 1 || x > 64 || *sep != ':') + fatal("Invalid time signature (%s)", arg); + ts_num = x; + x = strtol(++sep, NULL, 10); + if (x < 1 || x > 64) + fatal("Invalid time signature (%s)", arg); + ts_div = x; + for (ts_dd = 0; x > 1; x /= 2) + ++ts_dd; +} + +/* + * Metronome implementation + */ +static void metronome_note(unsigned char note, unsigned int tick) +{ + snd_seq_event_t ev; + snd_seq_ev_clear(&ev); + snd_seq_ev_set_note(&ev, metronome_channel, note, metronome_velocity, 1); + snd_seq_ev_schedule_tick(&ev, queue, 0, tick); + snd_seq_ev_set_source(&ev, port_count); + snd_seq_ev_set_subs(&ev); + snd_seq_event_output(seq, &ev); +} + +static void metronome_echo(unsigned int tick) +{ + snd_seq_event_t ev; + snd_seq_ev_clear(&ev); + ev.type = SND_SEQ_EVENT_USR0; + snd_seq_ev_schedule_tick(&ev, queue, 0, tick); + snd_seq_ev_set_source(&ev, port_count); + snd_seq_ev_set_dest(&ev, client, port_count); + snd_seq_event_output(seq, &ev); +} + +static void metronome_pattern(unsigned int tick) +{ + int j, t, duration; + + t = tick; + duration = ticks * 4 / ts_div; + for (j = 0; j < ts_num; j++) { + metronome_note(j ? metronome_weak_note : metronome_strong_note, t); + t += duration; + } + metronome_echo(t); + snd_seq_drain_output(seq); +} + +static void metronome_set_program(void) +{ + snd_seq_event_t ev; + + snd_seq_ev_clear(&ev); + snd_seq_ev_set_pgmchange(&ev, metronome_channel, metronome_program); + snd_seq_ev_set_source(&ev, port_count); + snd_seq_ev_set_subs(&ev); + snd_seq_event_output(seq, &ev); +} + +static void init_tracks(void) +{ + int i; + + /* MIDI RP-019 says we need at least one track per port */ + num_tracks = port_count; + /* Allocate one track for each possible channel. + * Empty tracks won't be written to the file. */ + if (channel_split) + num_tracks *= TRACKS_PER_PORT; + + tracks = calloc(num_tracks, sizeof(struct smf_track)); + check_mem(tracks); + for (i = 0; i < num_tracks; ++i) + tracks[i].cur_buf = &tracks[i].first_buf; +} + +static void create_queue(void) +{ + snd_seq_queue_tempo_t *tempo; + int err; + + queue = snd_seq_alloc_named_queue(seq, "arecordmidi"); + check_snd("create queue", queue); + + snd_seq_queue_tempo_alloca(&tempo); + if (!smpte_timing) { + snd_seq_queue_tempo_set_tempo(tempo, 60000000 / beats); + snd_seq_queue_tempo_set_ppq(tempo, ticks); + } else { + /* + * ALSA doesn't know about the SMPTE time divisions, so + * we pretend to have a musical tempo with the equivalent + * number of ticks/s. + */ + switch (frames) { + case 24: + snd_seq_queue_tempo_set_tempo(tempo, 500000); + snd_seq_queue_tempo_set_ppq(tempo, 12 * ticks); + break; + case 25: + snd_seq_queue_tempo_set_tempo(tempo, 400000); + snd_seq_queue_tempo_set_ppq(tempo, 10 * ticks); + break; + case 29: + snd_seq_queue_tempo_set_tempo(tempo, 100000000); + snd_seq_queue_tempo_set_ppq(tempo, 2997 * ticks); + break; + case 30: + snd_seq_queue_tempo_set_tempo(tempo, 500000); + snd_seq_queue_tempo_set_ppq(tempo, 15 * ticks); + break; + default: + fatal("Invalid SMPTE frames %d", frames); + } + } + err = snd_seq_set_queue_tempo(seq, queue, tempo); + if (err < 0) + fatal("Cannot set queue tempo (%u/%i)", + snd_seq_queue_tempo_get_tempo(tempo), + snd_seq_queue_tempo_get_ppq(tempo)); +} + +static void create_ports(void) +{ + snd_seq_port_info_t *pinfo; + int i, err; + char name[32]; + + snd_seq_port_info_alloca(&pinfo); + + /* common information for all our ports */ + snd_seq_port_info_set_capability(pinfo, + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE); + snd_seq_port_info_set_type(pinfo, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + snd_seq_port_info_set_midi_channels(pinfo, 16); + + /* we want to know when the events got delivered to us */ + snd_seq_port_info_set_timestamping(pinfo, 1); + snd_seq_port_info_set_timestamp_queue(pinfo, queue); + + /* our port number is the same as our port index */ + snd_seq_port_info_set_port_specified(pinfo, 1); + for (i = 0; i < port_count; ++i) { + snd_seq_port_info_set_port(pinfo, i); + + sprintf(name, "arecordmidi port %i", i); + snd_seq_port_info_set_name(pinfo, name); + + err = snd_seq_create_port(seq, pinfo); + check_snd("create port", err); + } + + /* create an optional metronome port */ + if (use_metronome) { + snd_seq_port_info_set_port(pinfo, port_count); + snd_seq_port_info_set_name(pinfo, "arecordmidi metronome"); + snd_seq_port_info_set_capability(pinfo, + SND_SEQ_PORT_CAP_READ | + SND_SEQ_PORT_CAP_WRITE); + snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_APPLICATION); + snd_seq_port_info_set_midi_channels(pinfo, 0); + snd_seq_port_info_set_timestamping(pinfo, 0); + err = snd_seq_create_port(seq, pinfo); + check_snd("create metronome port", err); + } +} + +static void connect_ports(void) +{ + int i, err; + + for (i = 0; i < port_count; ++i) { + err = snd_seq_connect_from(seq, i, ports[i].client, ports[i].port); + if (err < 0) + fatal("Cannot connect from port %d:%d - %s", + ports[i].client, ports[i].port, snd_strerror(err)); + } + + /* subscribe the metronome port */ + if (use_metronome) { + err = snd_seq_connect_to(seq, port_count, metronome_port.client, metronome_port.port); + if (err < 0) + fatal("Cannot connect to port %d:%d - %s", + metronome_port.client, metronome_port.port, snd_strerror(err)); + } +} + +/* records a byte to be written to the .mid file */ +static void add_byte(struct smf_track *track, unsigned char byte) +{ + /* make sure we have enough room in the current buffer */ + if (track->cur_buf_size >= BUFFER_SIZE) { + track->cur_buf->next = calloc(1, sizeof(struct buffer)); + if (!track->cur_buf->next) + fatal("out of memory"); + track->cur_buf = track->cur_buf->next; + track->cur_buf_size = 0; + } + + track->cur_buf->buf[track->cur_buf_size++] = byte; + track->size++; +} + +/* record a variable-length quantity */ +static void var_value(struct smf_track *track, int v) +{ + if (v >= (1 << 28)) + add_byte(track, 0x80 | ((v >> 28) & 0x03)); + if (v >= (1 << 21)) + add_byte(track, 0x80 | ((v >> 21) & 0x7f)); + if (v >= (1 << 14)) + add_byte(track, 0x80 | ((v >> 14) & 0x7f)); + if (v >= (1 << 7)) + add_byte(track, 0x80 | ((v >> 7) & 0x7f)); + add_byte(track, v & 0x7f); +} + +/* record the delta time from the last event */ +static void delta_time(struct smf_track *track, const snd_seq_event_t *ev) +{ + int diff = ev->time.tick - track->last_tick; + if (diff < 0) + diff = 0; + var_value(track, diff); + track->last_tick = ev->time.tick; +} + +/* record a status byte (or not if we can use running status) */ +static void command(struct smf_track *track, unsigned char cmd) +{ + if (cmd != track->last_command) + add_byte(track, cmd); + track->last_command = cmd < 0xf0 ? cmd : 0; +} + +/* put port numbers into all tracks */ +static void record_port_numbers(void) +{ + int i; + + for (i = 0; i < num_tracks; ++i) { + var_value(&tracks[i], 0); + add_byte(&tracks[i], 0xff); + add_byte(&tracks[i], 0x21); + var_value(&tracks[i], 1); + if (channel_split) + add_byte(&tracks[i], i / TRACKS_PER_PORT); + else + add_byte(&tracks[i], i); + } +} + +static void record_event(const snd_seq_event_t *ev) +{ + int i; + struct smf_track *track; + + /* ignore events without proper timestamps */ + if (ev->queue != queue || !snd_seq_ev_is_tick(ev)) + return; + + /* determine which track to record to */ + i = ev->dest.port; + if (i == port_count) { + if (ev->type == SND_SEQ_EVENT_USR0) + metronome_pattern(ev->time.tick); + return; + } + if (channel_split) { + i *= TRACKS_PER_PORT; + if (snd_seq_ev_is_channel_type(ev)) + i += 1 + (ev->data.note.channel & 0xf); + } + if (i >= num_tracks) + return; + track = &tracks[i]; + + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: + delta_time(track, ev); + command(track, MIDI_CMD_NOTE_ON | (ev->data.note.channel & 0xf)); + add_byte(track, ev->data.note.note & 0x7f); + add_byte(track, ev->data.note.velocity & 0x7f); + break; + case SND_SEQ_EVENT_NOTEOFF: + delta_time(track, ev); + command(track, MIDI_CMD_NOTE_OFF | (ev->data.note.channel & 0xf)); + add_byte(track, ev->data.note.note & 0x7f); + add_byte(track, ev->data.note.velocity & 0x7f); + break; + case SND_SEQ_EVENT_KEYPRESS: + delta_time(track, ev); + command(track, MIDI_CMD_NOTE_PRESSURE | (ev->data.note.channel & 0xf)); + add_byte(track, ev->data.note.note & 0x7f); + add_byte(track, ev->data.note.velocity & 0x7f); + break; + case SND_SEQ_EVENT_CONTROLLER: + delta_time(track, ev); + command(track, MIDI_CMD_CONTROL | (ev->data.control.channel & 0xf)); + add_byte(track, ev->data.control.param & 0x7f); + add_byte(track, ev->data.control.value & 0x7f); + break; + case SND_SEQ_EVENT_PGMCHANGE: + delta_time(track, ev); + command(track, MIDI_CMD_PGM_CHANGE | (ev->data.control.channel & 0xf)); + add_byte(track, ev->data.control.value & 0x7f); + break; + case SND_SEQ_EVENT_CHANPRESS: + delta_time(track, ev); + command(track, MIDI_CMD_CHANNEL_PRESSURE | (ev->data.control.channel & 0xf)); + add_byte(track, ev->data.control.value & 0x7f); + break; + case SND_SEQ_EVENT_PITCHBEND: + delta_time(track, ev); + command(track, MIDI_CMD_BENDER | (ev->data.control.channel & 0xf)); + add_byte(track, (ev->data.control.value + 8192) & 0x7f); + add_byte(track, ((ev->data.control.value + 8192) >> 7) & 0x7f); + break; + case SND_SEQ_EVENT_CONTROL14: + /* create two commands for MSB and LSB */ + delta_time(track, ev); + command(track, MIDI_CMD_CONTROL | (ev->data.control.channel & 0xf)); + add_byte(track, ev->data.control.param & 0x7f); + add_byte(track, (ev->data.control.value >> 7) & 0x7f); + if ((ev->data.control.param & 0x7f) < 0x20) { + delta_time(track, ev); + /* running status */ + add_byte(track, (ev->data.control.param & 0x7f) + 0x20); + add_byte(track, ev->data.control.value & 0x7f); + } + break; + case SND_SEQ_EVENT_NONREGPARAM: + delta_time(track, ev); + command(track, MIDI_CMD_CONTROL | (ev->data.control.channel & 0xf)); + add_byte(track, MIDI_CTL_NONREG_PARM_NUM_LSB); + add_byte(track, ev->data.control.param & 0x7f); + delta_time(track, ev); + add_byte(track, MIDI_CTL_NONREG_PARM_NUM_MSB); + add_byte(track, (ev->data.control.param >> 7) & 0x7f); + delta_time(track, ev); + add_byte(track, MIDI_CTL_MSB_DATA_ENTRY); + add_byte(track, (ev->data.control.value >> 7) & 0x7f); + delta_time(track, ev); + add_byte(track, MIDI_CTL_LSB_DATA_ENTRY); + add_byte(track, ev->data.control.value & 0x7f); + break; + case SND_SEQ_EVENT_REGPARAM: + delta_time(track, ev); + command(track, MIDI_CMD_CONTROL | (ev->data.control.channel & 0xf)); + add_byte(track, MIDI_CTL_REGIST_PARM_NUM_LSB); + add_byte(track, ev->data.control.param & 0x7f); + delta_time(track, ev); + add_byte(track, MIDI_CTL_REGIST_PARM_NUM_MSB); + add_byte(track, (ev->data.control.param >> 7) & 0x7f); + delta_time(track, ev); + add_byte(track, MIDI_CTL_MSB_DATA_ENTRY); + add_byte(track, (ev->data.control.value >> 7) & 0x7f); + delta_time(track, ev); + add_byte(track, MIDI_CTL_LSB_DATA_ENTRY); + add_byte(track, ev->data.control.value & 0x7f); + break; +#if 0 /* ignore */ + case SND_SEQ_EVENT_SONGPOS: + case SND_SEQ_EVENT_SONGSEL: + case SND_SEQ_EVENT_QFRAME: + case SND_SEQ_EVENT_START: + case SND_SEQ_EVENT_CONTINUE: + case SND_SEQ_EVENT_STOP: + case SND_SEQ_EVENT_TUNE_REQUEST: + case SND_SEQ_EVENT_RESET: + case SND_SEQ_EVENT_SENSING: + break; +#endif + case SND_SEQ_EVENT_SYSEX: + if (ev->data.ext.len == 0) + break; + delta_time(track, ev); + if (*(unsigned char*)ev->data.ext.ptr == 0xf0) + command(track, 0xf0), i = 1; + else + command(track, 0xf7), i = 0; + var_value(track, ev->data.ext.len - i); + for (; i < (int)ev->data.ext.len; ++i) + add_byte(track, ((unsigned char*)ev->data.ext.ptr)[i]); + break; + default: + return; + } + track->used = 1; +} + +static void finish_tracks(void) +{ + snd_seq_queue_status_t *queue_status; + int tick, i, err; + + snd_seq_queue_status_alloca(&queue_status); + + err = snd_seq_get_queue_status(seq, queue, queue_status); + check_snd("get queue status", err); + tick = snd_seq_queue_status_get_tick_time(queue_status); + + /* make length of first track the recording length */ + var_value(&tracks[0], tick - tracks[0].last_tick); + add_byte(&tracks[0], 0xff); + add_byte(&tracks[0], 0x2f); + var_value(&tracks[0], 0); + + /* finish other tracks */ + for (i = 1; i < num_tracks; ++i) { + var_value(&tracks[i], 0); + add_byte(&tracks[i], 0xff); + add_byte(&tracks[i], 0x2f); + var_value(&tracks[i], 0); + } +} + +static void write_file(void) +{ + int used_tracks, time_division, i; + struct buffer *buf; + + used_tracks = 0; + for (i = 0; i < num_tracks; ++i) + used_tracks += !!tracks[i].used; + + /* header id and length */ + fwrite("MThd\0\0\0\6", 1, 8, file); + /* type 0 or 1 */ + fputc(0, file); + fputc(used_tracks > 1 ? 1 : 0, file); + /* number of tracks */ + fputc((used_tracks >> 8) & 0xff, file); + fputc(used_tracks & 0xff, file); + /* time division */ + time_division = ticks; + if (smpte_timing) + time_division |= (0x100 - frames) << 8; + fputc(time_division >> 8, file); + fputc(time_division & 0xff, file); + + for (i = 0; i < num_tracks; ++i) { + if (!tracks[i].used) + continue; + /* track id */ + fwrite("MTrk", 1, 4, file); + /* data length */ + fputc((tracks[i].size >> 24) & 0xff, file); + fputc((tracks[i].size >> 16) & 0xff, file); + fputc((tracks[i].size >> 8) & 0xff, file); + fputc(tracks[i].size & 0xff, file); + /* track contents */ + for (buf = &tracks[i].first_buf; buf; buf = buf->next) + fwrite(buf->buf, 1, buf == tracks[i].cur_buf + ? tracks[i].cur_buf_size : BUFFER_SIZE, file); + } +} + +static void list_ports(void) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + puts(" Port Client name Port name"); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + + if (client == SND_SEQ_CLIENT_SYSTEM) + continue; /* don't show system timer and announce ports */ + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + /* port must understand MIDI messages */ + if (!(snd_seq_port_info_get_type(pinfo) + & SND_SEQ_PORT_TYPE_MIDI_GENERIC)) + continue; + /* we need both READ and SUBS_READ */ + if ((snd_seq_port_info_get_capability(pinfo) + & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) + != (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) + continue; + printf("%3d:%-3d %-32.32s %s\n", + snd_seq_port_info_get_client(pinfo), + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } +} + +static void help(const char *argv0) +{ + fprintf(stderr, "Usage: %s [options] outputfile\n" + "\nAvailable options:\n" + " -h,--help this help\n" + " -V,--version show version\n" + " -l,--list list input ports\n" + " -p,--port=client:port,... source port(s)\n" + " -b,--bpm=beats tempo in beats per minute\n" + " -f,--fps=frames resolution in frames per second (SMPTE)\n" + " -t,--ticks=ticks resolution in ticks per beat or frame\n" + " -s,--split-channels create a track for each channel\n" + " -m,--metronome=client:port play a metronome signal\n" + " -i,--timesig=nn:dd time signature\n" + " -n,--num-events=events fixed number of events to record, then exit\n", + argv0); +} + +static void version(void) +{ + fputs("arecordmidi version " SND_UTIL_VERSION_STR "\n", stderr); +} + +static void sighandler(int sig ATTRIBUTE_UNUSED) +{ + stop = 1; +} + +int main(int argc, char *argv[]) +{ + static const char short_options[] = "hVlp:b:f:t:sdm:i:n:"; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"list", 0, NULL, 'l'}, + {"port", 1, NULL, 'p'}, + {"bpm", 1, NULL, 'b'}, + {"fps", 1, NULL, 'f'}, + {"ticks", 1, NULL, 't'}, + {"split-channels", 0, NULL, 's'}, + {"dump", 0, NULL, 'd'}, + {"metronome", 1, NULL, 'm'}, + {"timesig", 1, NULL, 'i'}, + {"num-events", 1, NULL, 'n'}, + {0} + }; + + char *filename = NULL; + int do_list = 0; + struct pollfd *pfds; + int npfds; + int c, err; + /* If |num_events| isn't specified, leave it at 0. */ + long num_events = 0; + long events_received = 0; + + init_seq(); + + while ((c = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (c) { + case 'h': + help(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'l': + do_list = 1; + break; + case 'p': + parse_ports(optarg); + break; + case 'b': + beats = atoi(optarg); + if (beats < 4 || beats > 6000) + fatal("Invalid tempo"); + smpte_timing = 0; + break; + case 'f': + frames = atoi(optarg); + if (frames != 24 && frames != 25 && + frames != 29 && frames != 30) + fatal("Invalid number of frames/s"); + smpte_timing = 1; + break; + case 't': + ticks = atoi(optarg); + if (ticks < 1 || ticks > 0x7fff) + fatal("Invalid number of ticks"); + break; + case 's': + channel_split = 1; + break; + case 'd': + fputs("The --dump option isn't supported anymore, use aseqdump instead.\n", stderr); + break; + case 'm': + init_metronome(optarg); + break; + case 'i': + time_signature(optarg); + break; + case 'n': + err = 0; + num_events = arg_parse_decimal_num(optarg, &err); + if (err != 0) { + fatal("Couldn't parse num_events argument: %s\n", + strerror(-err)); + } + if (num_events <= 0) + fatal("num_events must be greater than 0"); + break; + default: + help(argv[0]); + return 1; + } + } + + if (do_list) { + list_ports(); + return 0; + } + + if (port_count < 1) { + fputs("Pleast specify a source port with --port.\n", stderr); + return 1; + } + + if (!ticks) + ticks = smpte_timing ? 40 : 384; + if (smpte_timing && ticks > 0xff) + ticks = 0xff; + + if (optind >= argc) { + fputs("Please specify a file to record to.\n", stderr); + return 1; + } + filename = argv[optind]; + + init_tracks(); + create_queue(); + create_ports(); + connect_ports(); + if (port_count > 1) + record_port_numbers(); + + /* record tempo */ + if (!smpte_timing) { + int usecs_per_quarter = 60000000 / beats; + var_value(&tracks[0], 0); /* delta time */ + add_byte(&tracks[0], 0xff); + add_byte(&tracks[0], 0x51); + var_value(&tracks[0], 3); + add_byte(&tracks[0], usecs_per_quarter >> 16); + add_byte(&tracks[0], usecs_per_quarter >> 8); + add_byte(&tracks[0], usecs_per_quarter); + + /* time signature */ + var_value(&tracks[0], 0); /* delta time */ + add_byte(&tracks[0], 0xff); + add_byte(&tracks[0], 0x58); + var_value(&tracks[0], 4); + add_byte(&tracks[0], ts_num); + add_byte(&tracks[0], ts_dd); + add_byte(&tracks[0], 24); /* MIDI clocks per metronome click */ + add_byte(&tracks[0], 8); /* notated 32nd-notes per MIDI quarter note */ + } + + /* always write at least one track */ + tracks[0].used = 1; + + file = fopen(filename, "wb"); + if (!file) + fatal("Cannot open %s - %s", filename, strerror(errno)); + + err = snd_seq_start_queue(seq, queue, NULL); + check_snd("start queue", err); + snd_seq_drain_output(seq); + + err = snd_seq_nonblock(seq, 1); + check_snd("set nonblock mode", err); + + if (use_metronome) { + metronome_set_program(); + metronome_pattern(0); + } + + signal(SIGINT, sighandler); + signal(SIGTERM, sighandler); + + npfds = snd_seq_poll_descriptors_count(seq, POLLIN); + pfds = alloca(sizeof(*pfds) * npfds); + for (;;) { + snd_seq_poll_descriptors(seq, pfds, npfds, POLLIN); + if (poll(pfds, npfds, -1) < 0) + break; + do { + snd_seq_event_t *event; + err = snd_seq_event_input(seq, &event); + if (err < 0) + break; + if (event) { + record_event(event); + events_received++; + } + } while (err > 0); + if (stop) + break; + if (num_events && (events_received == num_events)) + break; + } + + if (num_events && events_received < num_events) + fputs("Warning: Received signal before num_events\n", stdout); + + finish_tracks(); + write_file(); + + fclose(file); + snd_seq_close(seq); + return 0; +} diff --git a/seq/aplaymidi2/Makefile.am b/seq/aplaymidi2/Makefile.am new file mode 100644 index 0000000..8985382 --- /dev/null +++ b/seq/aplaymidi2/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = aplaymidi2.1 arecordmidi2.1 + +bin_PROGRAMS = aplaymidi2 arecordmidi2 +man_MANS = aplaymidi2.1 arecordmidi2.1 diff --git a/seq/aplaymidi2/aplaymidi2.1 b/seq/aplaymidi2/aplaymidi2.1 new file mode 100644 index 0000000..6817962 --- /dev/null +++ b/seq/aplaymidi2/aplaymidi2.1 @@ -0,0 +1,84 @@ +.TH APLAYMIDI2 1 "4 July 2024" + +.SH NAME +aplaymidi2 \- play MIDI Clip Files + +.SH SYNOPSIS +.B aplaymidi2 +\-p client:port[,...] midi2file ... + +.SH DESCRIPTION +.B aplaymidi2 +is a command-line utility that plays the specified MIDI Clip file(s) to one +or more ALSA sequencer ports. + +.SH OPTIONS + +.TP +.I \-h, \-\-help +Prints a list of options. + +.TP +.I \-V, \-\-version +Prints the current version. + +.TP +.I \-p, \-\-port=client:port,... +Sets the sequencer port(s) to which the events in the MIDI Clip file(s) are +sent. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +Multiple ports can be specified to allow playback of MIDI Clip file(s) that +contain events for multiple devices (ports) corresponding to the +multiple UMP Groups. + +For compatibility with +.B pmidi(1), +the port specification is taken from the +.I ALSA_OUTPUT_PORTS +environment variable if none is given on the command line. + +.B aplaymidi2 +supports only basic UMP events: in addition to the standard MIDI1 and +MIDI2 CVMs and 7bit SysEx, only the following are supported: +DCTPQ, DC, Set Tempo, Start Clip, End Clip. +Lyrics and other meta data in Flex Data are printed, too, unless +\fI\-s\fP option is given. + +The multiple output ports are useful when the given MIDI Clip file +contains the UMP packets for multiple Groups. +When the destination port is a UMP MIDI 2.0 port, the single +connection should suffice, though, since a MIDI 2.0 port can process +the inputs for multiple Groups. For other cases (e.g. connecting to a +legacy MIDI port), you would need to specify the destination port per +Group. If undefined, it's sent to the first destination port as +default. + +.TP +.I \-d, \-\-delay=seconds +Specifies how long to wait after the end of each MIDI Clip file, +to allow the last notes to die away. +Default is 2 seconds. + +.TP +.I \-s, \-\-silent +Don't show message texts. + +.TP +.I \-a, \-\-passall +Pass all UMP packets as is. + +As default, \fBaplaymidi2\fP passes only MIDI1 and MIDI2 channel voice +messages and process other UMP packets internally. +With this option, it passes all UMP packets to the target. + +.SH SEE ALSO +pmidi(1) +.br +aplaymidi(1) + +.SH AUTHOR +Takashi Iwai diff --git a/seq/aplaymidi2/aplaymidi2.c b/seq/aplaymidi2/aplaymidi2.c new file mode 100644 index 0000000..f71958d --- /dev/null +++ b/seq/aplaymidi2/aplaymidi2.c @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * aplaymidi2.c - simple player of a MIDI Clip File over ALSA sequencer + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "version.h" + +static snd_seq_t *seq; +static int client; +static int port_count; +static snd_seq_addr_t ports[16]; +static int queue; +static int end_delay = 2; +static int silent; +static int passall; + +static unsigned int _current_tempo = 50000000; /* default 120 bpm */ +static unsigned int tempo_base = 10; +static unsigned int current_tick; + +/* prints an error message to stderr */ +static void errormsg(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +/* memory allocation error handling */ +static void check_mem(void *p) +{ + if (!p) + fatal("Out of memory"); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +/* open and initialize the sequencer client */ +static void init_seq(void) +{ + int err; + + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + err = snd_seq_set_client_name(seq, "aplaymidi2"); + check_snd("set client name", err); + + client = snd_seq_client_id(seq); + check_snd("get client id", client); + + err = snd_seq_set_client_midi_version(seq, SND_SEQ_CLIENT_UMP_MIDI_2_0); + check_snd("set midi version", err); +} + +/* parses one or more port addresses from the string */ +static void parse_ports(const char *arg) +{ + char *buf, *s, *port_name; + int err; + + /* make a copy of the string because we're going to modify it */ + buf = strdup(arg); + check_mem(buf); + + for (port_name = s = buf; s; port_name = s + 1) { + /* Assume that ports are separated by commas. We don't use + * spaces because those are valid in client names. */ + s = strchr(port_name, ','); + if (s) + *s = '\0'; + + ++port_count; + if (port_count > 16) + fatal("Too many ports specified"); + + err = snd_seq_parse_address(seq, &ports[port_count - 1], port_name); + if (err < 0) + fatal("Invalid port %s - %s", port_name, snd_strerror(err)); + } + + free(buf); +} + +/* create a source port to send from */ +static void create_source_port(void) +{ + snd_seq_port_info_t *pinfo; + int err; + + snd_seq_port_info_alloca(&pinfo); + + /* the first created port is 0 anyway, but let's make sure ... */ + snd_seq_port_info_set_port(pinfo, 0); + snd_seq_port_info_set_port_specified(pinfo, 1); + + snd_seq_port_info_set_name(pinfo, "aplaymidi2"); + + snd_seq_port_info_set_capability(pinfo, 0); /* sic */ + snd_seq_port_info_set_type(pinfo, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + + err = snd_seq_create_port(seq, pinfo); + check_snd("create port", err); +} + +/* create a queue */ +static void create_queue(void) +{ + if (!snd_seq_has_queue_tempo_base(seq)) + tempo_base = 1000; + + queue = snd_seq_alloc_named_queue(seq, "aplaymidi2"); + check_snd("create queue", queue); +} + +/* connect to destination ports */ +static void connect_ports(void) +{ + int i, err; + + for (i = 0; i < port_count; ++i) { + err = snd_seq_connect_to(seq, 0, ports[i].client, ports[i].port); + if (err < 0) + fatal("Cannot connect to port %d:%d - %s", + ports[i].client, ports[i].port, snd_strerror(err)); + } +} + +/* read 32bit word and convert to native endian: + * return 0 on success, -1 on error + */ +static int read_word(FILE *file, uint32_t *dest) +{ + uint32_t v; + + if (fread(&v, 4, 1, file) != 1) + return -1; + *dest = be32toh(v); + return 0; +} + +/* read a UMP packet: return the number of packets, -1 on error */ +static int read_ump_packet(FILE *file, uint32_t *buf) +{ + snd_ump_msg_hdr_t *h = (snd_ump_msg_hdr_t *)buf; + + int i, num; + + if (read_word(file, buf) < 0) + return -1; + num = snd_ump_packet_length(h->type); + for (i = 1; i < num; i++) { + if (read_word(file, buf + i) < 0) + return -1; + } + return num; +} + +/* read the file header and verify it's MIDI Clip File: return 0 on success */ +static int verify_file_header(FILE *file) +{ + unsigned char buf[8]; + + if (fread(buf, 1, 8, file) != 8) + return -1; + if (memcmp(buf, "SMF2CLIP", 8)) + return -1; + return 0; +} + +/* return the current tempo, corrected to be sent to host */ +static int current_tempo(void) +{ + if (tempo_base != 10) + return _current_tempo / 100; /* down to us */ + return _current_tempo; +} + +/* send a timer event */ +static void send_timer_event(unsigned int type, unsigned int val) +{ + snd_seq_ump_event_t ev = { + .type = type, + .flags = SND_SEQ_TIME_STAMP_TICK | SND_SEQ_EVENT_LENGTH_FIXED, + }; + + ev.queue = queue; + ev.source.port = 0; + ev.time.tick = current_tick; + + ev.dest.client = SND_SEQ_CLIENT_SYSTEM; + ev.dest.port = SND_SEQ_PORT_SYSTEM_TIMER; + ev.data.queue.queue = queue; + ev.data.queue.param.value = val; + + snd_seq_ump_event_output(seq, &ev); +} + +/* set DCTPQ */ +static void set_dctpq(unsigned int ppq) +{ + snd_seq_queue_tempo_t *queue_tempo; + + snd_seq_queue_tempo_alloca(&queue_tempo); + snd_seq_queue_tempo_set_tempo(queue_tempo, current_tempo()); + snd_seq_queue_tempo_set_ppq(queue_tempo, ppq); + snd_seq_queue_tempo_set_tempo_base(queue_tempo, tempo_base); + + if (snd_seq_set_queue_tempo(seq, queue, queue_tempo) < 0) + errormsg("Cannot set queue tempo (%d)", queue); +} + +/* set DC */ +static void set_dc(unsigned int ticks) +{ + current_tick += ticks; +} + +/* set tempo event */ +static void set_tempo(unsigned int tempo) +{ + _current_tempo = tempo; + send_timer_event(SND_SEQ_EVENT_TEMPO, current_tempo()); +} + +/* start clip */ +static void start_clip(void) +{ + if (snd_seq_start_queue(seq, queue, NULL) < 0) + errormsg("Cannot start queue (%d)", queue); +} + +/* end clip */ +static void end_clip(void) +{ + send_timer_event(SND_SEQ_EVENT_STOP, 0); +} + +/* send a UMP packet */ +static void send_ump(const uint32_t *ump, int len) +{ + snd_seq_ump_event_t ev = { + .flags = SND_SEQ_TIME_STAMP_TICK | SND_SEQ_EVENT_LENGTH_FIXED | + SND_SEQ_EVENT_UMP, + }; + int group; + + memcpy(ev.ump, ump, len * 4); + + ev.queue = queue; + ev.source.port = 0; + ev.time.tick = current_tick; + group = snd_ump_msg_group(ump); + if (group >= port_count) + ev.dest = ports[0]; + else + ev.dest = ports[group]; + + snd_seq_ump_event_output(seq, &ev); +} + +struct flexdata_text_prefix { + unsigned char status_bank; + unsigned char status; + const char *prefix; +}; + +static struct flexdata_text_prefix text_prefix[] = { + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_PROJECT_NAME, + .prefix = "Project" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_SONG_NAME, + .prefix = "Song" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_MIDI_CLIP_NAME, + .prefix = "MIDI Clip" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_COPYRIGHT_NOTICE, + .prefix = "Copyright" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_COMPOSER_NAME, + .prefix = "Composer" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_LYRICIST_NAME, + .prefix = "Lyricist" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_ARRANGER_NAME, + .prefix = "Arranger" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_PUBLISHER_NAME, + .prefix = "Publisher" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_PRIMARY_PERFORMER, + .prefix = "Performer" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_ACCOMPANY_PERFORMAER, + .prefix = "Accompany Performer" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RECORDING_DATE, + .prefix = "Recording Date" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RECORDING_LOCATION, + .prefix = "Recording Location" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_LYRICS, + .prefix = "Lyrics" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_LYRICS_LANGUAGE, + .prefix = "Lyrics Language" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RUBY, + .prefix = "Ruby" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RUBY_LANGUAGE, + .prefix = "Ruby Language" }, + {} +}; + +static void show_text(const uint32_t *ump) +{ + static unsigned char textbuf[256]; + static int len; + const snd_ump_msg_flex_data_t *fh = + (const snd_ump_msg_flex_data_t *)ump; + const char *prefix; + int i; + + if (fh->meta.format == SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE || + fh->meta.format == SND_UMP_FLEX_DATA_MSG_FORMAT_START) + len = 0; + + for (i = 0; i < 12 && len < (int)sizeof(textbuf); i++) { + textbuf[len] = snd_ump_get_byte(ump, 4 + i); + if (!textbuf[len]) + break; + switch (textbuf[len]) { + case 0x0a: /* end of paragraph */ + case 0x0d: /* end of line */ + textbuf[len] = '\n'; + break; + } + len++; + } + + if (fh->meta.format != SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE && + fh->meta.format != SND_UMP_FLEX_DATA_MSG_FORMAT_END) + return; + + if (len >= (int)sizeof(textbuf)) + len = sizeof(textbuf) - 1; + textbuf[len] = 0; + + prefix = NULL; + for (i = 0; text_prefix[i].status_bank; i++) { + if (text_prefix[i].status_bank == fh->meta.status_bank && + text_prefix[i].status == fh->meta.status) { + prefix = text_prefix[i].prefix; + break; + } + } + + if (prefix) { + printf("%s: %s\n", prefix, textbuf); + } else { + printf("(%d:%d): %s\n", fh->meta.status_bank, fh->meta.status, + textbuf); + } + + len = 0; +} + +/* play the given MIDI Clip File content */ +static void play_midi(FILE *file) +{ + uint32_t ump[4]; + int len; + + current_tick = 0; + + while ((len = read_ump_packet(file, ump)) > 0) { + const snd_ump_msg_hdr_t *h = (snd_ump_msg_hdr_t *)ump; + + if (passall) + send_ump(ump, len); + + if (h->type == SND_UMP_MSG_TYPE_UTILITY) { + const snd_ump_msg_utility_t *uh = + (const snd_ump_msg_utility_t *)ump; + switch (h->status) { + case SND_UMP_UTILITY_MSG_STATUS_DCTPQ: + set_dctpq(uh->dctpq.ticks); + continue; + case SND_UMP_UTILITY_MSG_STATUS_DC: + set_dc(uh->dctpq.ticks); + continue; + } + } else if (h->type == SND_UMP_MSG_TYPE_FLEX_DATA) { + const snd_ump_msg_flex_data_t *fh = + (const snd_ump_msg_flex_data_t *)ump; + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_TEMPO) { + set_tempo(fh->set_tempo.tempo); + continue; + } + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_METADATA || + fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT) { + if (!silent) + show_text(ump); + continue; + } + } else if (h->type == SND_UMP_MSG_TYPE_STREAM) { + const snd_ump_msg_stream_t *sh = + (const snd_ump_msg_stream_t *)ump; + switch (sh->gen.status) { + case SND_UMP_STREAM_MSG_STATUS_START_CLIP: + start_clip(); + continue; + case SND_UMP_STREAM_MSG_STATUS_END_CLIP: + end_clip(); + continue; + } + } else if (!passall && + (h->type == SND_UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE || + h->type == SND_UMP_MSG_TYPE_DATA || + h->type == SND_UMP_MSG_TYPE_MIDI2_CHANNEL_VOICE)) { + send_ump(ump, len); + } + } + + snd_seq_drain_output(seq); + snd_seq_sync_output_queue(seq); + + /* give the last notes time to die away */ + if (end_delay > 0) + sleep(end_delay); +} + +static void play_file(const char *file_name) +{ + FILE *file; + + if (!strcmp(file_name, "-")) + file = stdin; + else + file = fopen(file_name, "rb"); + if (!file) { + errormsg("Cannot open %s - %s", file_name, strerror(errno)); + return; + } + + if (verify_file_header(file) < 0) { + errormsg("%s is not a MIDI Clip File", file_name); + goto error; + } + + play_midi(file); + + error: + if (file != stdin) + fclose(file); +} + +static void usage(const char *argv0) +{ + printf( + "Usage: %s -p client:port[,...] [-d delay] midifile ...\n" + "-h, --help this help\n" + "-V, --version print current version\n" + "-p, --port=client:port,... set port(s) to play to\n" + "-d, --delay=seconds delay after song ends\n" + "-s, --silent don't show texts\n" + "-a, --passall pass all UMP packets as-is\n", + argv0); +} + +static void version(void) +{ + puts("aplaymidi2 version " SND_UTIL_VERSION_STR); +} + +int main(int argc, char *argv[]) +{ + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"port", 1, NULL, 'p'}, + {"delay", 1, NULL, 'd'}, + {"silent", 0, NULL, 's'}, + {"passall", 0, NULL, 'a'}, + {0} + }; + int c; + + init_seq(); + + while ((c = getopt_long(argc, argv, "hVp:d:sa", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'p': + parse_ports(optarg); + break; + case 'd': + end_delay = atoi(optarg); + break; + case 's': + silent = 1; + break; + case 'a': + passall = 1; + break; + default: + usage(argv[0]); + return 1; + } + } + + + if (port_count < 1) { + /* use env var for compatibility with pmidi */ + const char *ports_str = getenv("ALSA_OUTPUT_PORTS"); + if (ports_str) + parse_ports(ports_str); + if (port_count < 1) { + errormsg("Please specify at least one port with --port."); + return 1; + } + } + if (optind >= argc) { + errormsg("Please specify a file to play."); + return 1; + } + + create_source_port(); + create_queue(); + connect_ports(); + + for (; optind < argc; optind++) + play_file(argv[optind]); + + snd_seq_close(seq); + return 0; +} diff --git a/seq/aplaymidi2/arecordmidi2.1 b/seq/aplaymidi2/arecordmidi2.1 new file mode 100644 index 0000000..c20db8d --- /dev/null +++ b/seq/aplaymidi2/arecordmidi2.1 @@ -0,0 +1,110 @@ +.TH ARECORDMIDI2 1 "4 July 2024" + +.SH NAME +arecordmidi2 \- record a MIDI Clip file + +.SH SYNOPSIS +.B arecordmidi2 +[options] midi2file + +.SH DESCRIPTION +.B arecordmidi2 +is a command-line utility that records a MIDI Clip file from one or +more ALSA sequencer ports. + +To stop recording, press Ctrl+C. + +When \fB\-\fP is passed to the MIDI Clip file argument, +it's recorded to stdout. It implies \fI\-s\fP option, too. + +.SH OPTIONS + +.TP +.I \-h,\-\-help +Prints a list of options. + +.TP +.I \-V,\-\-version +Prints the current version. + +.TP +.I \-p,\-\-port=client:port,... +Sets the sequencer port(s) from which events are recorded. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +\fBarecordmidi2\fP creates a UMP Endpoint containing the same number +of Function Blocks as specified by this option, each of which is +connected to the specified port as a source. + +When no source ports are specified with \fI\-p\fP option, +\fBarecordmidi2\fP creates a UMP Endpoint with full 16 Function Blocks +and records from those inputs. User can connect the sequencer ports +freely via \fBaconnect\fP, for example. This mode can be used +together with the interactive mode via \fI\-r\fP option. + +.TP +.I \-b,\-\-bpm=beats +Sets the musical tempo of the MIDI file, in beats per minute. +The default value is 120 BPM. + +.TP +.I \-t,\-\-ticks=ticks +Sets the resolution of timestamps (ticks) in the MIDI file, +in ticks per beat. +The default value is 384 ticks/beat. + +.TP +.I \-i,\-\-timesig=numerator:denominator +Sets the time signature for the MIDI file. + +The time signature is specified as usual with two numbers, representing +the numerator and denominator of the time signature as it would be +notated. The denominator must be a power of two. Both numbers should be +separated by a colon. The time signature is 4:4 by default. + +.TP +.I \-n,\-\-num-events=events +Stops the recording after receiving the given number of events. + +.TP +.I \-u,\-\-ump=version +Sets the UMP MIDI protocol version. Either 1 or 2 has to be given for +MIDI 1.0 and MIDI 2.0 protocol, respectively. +Default is 1. + +.TP +.I \-r,\-\-interactive +Run in the interactive mode. \fBarecordmidi2\fP waits for a RETURN +key input from the terminal to start the recording. After starting, +the recording ends when another RETURN key is input from the +terminal. The received events before the start of recording are +discarded. + +.TP +.I \-s,\-\-silent +Don't print messages to stdout. + +.TP +.I \-P,\-\-profile=file +Read the UMP data from the given file and put them into the +configuration section of the recorded output. +The file must contain only valid UMP data encoded in big-endian. + +.TP +.I \-\-song=text, \-\-clip=text, \-\-copyright=text, \-\-composer=text, \ +\-\-lyricist=text, \-\-arranger=text, \-\-publisher=text, \ +\-\-performer=text \-\-accompany=text, \-\-date=text, \-\-location=text +Put the given meta data text in the configuration section. + +.SH SEE ALSO +arecordmidi(1) +.br +aplaymidi2(1) + +.SH AUTHOR +Takashi Iwai + + diff --git a/seq/aplaymidi2/arecordmidi2.c b/seq/aplaymidi2/arecordmidi2.c new file mode 100644 index 0000000..467af65 --- /dev/null +++ b/seq/aplaymidi2/arecordmidi2.c @@ -0,0 +1,717 @@ +/* + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aconfig.h" +#include "version.h" + +static snd_seq_t *seq; +static int client; +static int port_count; +static snd_seq_addr_t *ports; +static int queue; +static int midi_version = 1; +static int beats = 120; +static int ticks = 384; +static int tempo_base = 10; +static volatile sig_atomic_t stop; +static int ts_num = 4; /* time signature: numerator */ +static int ts_div = 4; /* time signature: denominator */ +static int last_tick; +static int silent; +static const char *profile_ump_file; + +#define MAX_METADATA 16 +static int metadata_num; +static unsigned int metadata_types[MAX_METADATA]; +static const char *metadata_texts[MAX_METADATA]; + +/* Parse a decimal number from a command line argument. */ +static long arg_parse_decimal_num(const char *str, int *err) +{ + long val; + char *endptr; + + errno = 0; + val = strtol(str, &endptr, 0); + if (errno > 0) { + *err = -errno; + return 0; + } + if (*endptr != '\0') { + *err = -EINVAL; + return 0; + } + + return val; +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +/* memory allocation error handling */ +static void check_mem(void *p) +{ + if (!p) + fatal("Out of memory"); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +/* open a sequencer client */ +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + /* find out our client's id */ + client = snd_seq_client_id(seq); + check_snd("get client id", client); +} + +/* set up UMP virtual client/port */ +static void create_ump_client(void) +{ + snd_ump_endpoint_info_t *ep; + snd_ump_block_info_t *blk; + snd_seq_port_info_t *pinfo; + int num_groups; + int i, err; + + /* in passive mode, create full 16 groups */ + if (port_count) + num_groups = port_count; + else + num_groups = 16; + + /* create a UMP Endpoint */ + snd_ump_endpoint_info_alloca(&ep); + snd_ump_endpoint_info_set_name(ep, "arecordmidi2"); + if (midi_version == 1) { + snd_ump_endpoint_info_set_protocol_caps(ep, SND_UMP_EP_INFO_PROTO_MIDI1); + snd_ump_endpoint_info_set_protocol(ep, SND_UMP_EP_INFO_PROTO_MIDI1); + } else { + snd_ump_endpoint_info_set_protocol_caps(ep, SND_UMP_EP_INFO_PROTO_MIDI2); + snd_ump_endpoint_info_set_protocol(ep, SND_UMP_EP_INFO_PROTO_MIDI2); + } + snd_ump_endpoint_info_set_num_blocks(ep, num_groups); + + err = snd_seq_create_ump_endpoint(seq, ep, num_groups); + check_snd("create UMP endpoint", err); + + /* create UMP Function Blocks */ + snd_ump_block_info_alloca(&blk); + for (i = 0; i < num_groups; i++) { + char blkname[32]; + + sprintf(blkname, "Group %d", i + 1); + snd_ump_block_info_set_name(blk, blkname); + snd_ump_block_info_set_direction(blk, SND_UMP_DIR_INPUT); + snd_ump_block_info_set_first_group(blk, i); + snd_ump_block_info_set_num_groups(blk, 1); + snd_ump_block_info_set_ui_hint(blk, SND_UMP_BLOCK_UI_HINT_RECEIVER); + + err = snd_seq_create_ump_block(seq, i, blk); + check_snd("create UMP block", err); + } + + /* toggle timestamping for all input ports */ + snd_seq_port_info_alloca(&pinfo); + for (i = 0; i <= num_groups; i++) { + err = snd_seq_get_port_info(seq, i, pinfo); + check_snd("get port info", err); + snd_seq_port_info_set_timestamping(pinfo, 1); + snd_seq_port_info_set_timestamp_queue(pinfo, queue); + snd_seq_set_port_info(seq, i, pinfo); + check_snd("set port info", err); + } +} + +/* parses one or more port addresses from the string */ +static void parse_ports(const char *arg) +{ + char *buf, *s, *port_name; + int err; + + /* make a copy of the string because we're going to modify it */ + buf = strdup(arg); + check_mem(buf); + + for (port_name = s = buf; s; port_name = s + 1) { + /* Assume that ports are separated by commas. We don't use + * spaces because those are valid in client names. + */ + s = strchr(port_name, ','); + if (s) + *s = '\0'; + + ++port_count; + ports = realloc(ports, port_count * sizeof(snd_seq_addr_t)); + check_mem(ports); + + err = snd_seq_parse_address(seq, &ports[port_count - 1], port_name); + if (err < 0) + fatal("Invalid port %s - %s", port_name, snd_strerror(err)); + } + + free(buf); +} + +/* parses time signature specification */ +static void time_signature(const char *arg) +{ + long x = 0; + char *sep; + + x = strtol(arg, &sep, 10); + if (x < 1 || x > 64 || *sep != ':') + fatal("Invalid time signature (%s)", arg); + ts_num = x; + x = strtol(++sep, NULL, 10); + if (x < 1 || x > 64) + fatal("Invalid time signature (%s)", arg); + ts_div = x; +} + +/* create a queue, set up the default tempo */ +static void create_queue(void) +{ + snd_seq_queue_tempo_t *tempo; + + if (!snd_seq_has_queue_tempo_base(seq)) + tempo_base = 1000; + + queue = snd_seq_alloc_named_queue(seq, "arecordmidi2"); + check_snd("create queue", queue); + + snd_seq_queue_tempo_alloca(&tempo); + if (tempo_base == 1000) + snd_seq_queue_tempo_set_tempo(tempo, 60000000 / beats); + else + snd_seq_queue_tempo_set_tempo(tempo, (unsigned int)(6000000000ULL / beats)); + snd_seq_queue_tempo_set_ppq(tempo, ticks); + snd_seq_queue_tempo_set_tempo_base(tempo, tempo_base); + if (snd_seq_set_queue_tempo(seq, queue, tempo) < 0) + fatal("Cannot set queue tempo (%d)", queue); +} + +/* connect to the input ports */ +static void connect_ports(void) +{ + int i, err; + + for (i = 0; i < port_count; ++i) { + err = snd_seq_connect_from(seq, i + 1, + ports[i].client, ports[i].port); + check_snd("port connection", err); + } +} + +/* write the given UMP packet */ +static void write_ump(FILE *file, const void *src) +{ + const snd_ump_msg_hdr_t *h = src; + const uint32_t *p = src; + uint32_t v; + int len; + + len = snd_ump_packet_length(h->type); + while (len-- > 0) { + v = htobe32(*p++); + fwrite(&v, 4, 1, file); + } +} + +/* write a DC message */ +static void write_dcs(FILE *file, unsigned int t) +{ + snd_ump_msg_dc_t d = {}; + + d.type = SND_UMP_MSG_TYPE_UTILITY; + d.status = SND_UMP_UTILITY_MSG_STATUS_DC; + d.ticks = t; + write_ump(file, &d); +} + +/* write a DCTPQ message */ +static void write_dctpq(FILE *file) +{ + snd_ump_msg_dctpq_t d = {}; + + d.type = SND_UMP_MSG_TYPE_UTILITY; + d.status = SND_UMP_UTILITY_MSG_STATUS_DCTPQ; + d.ticks = ticks; + write_ump(file, &d); +} + +/* write a Start Clip message */ +static void write_start_clip(FILE *file) +{ + snd_ump_msg_stream_gen_t d = {}; + + d.type = SND_UMP_MSG_TYPE_STREAM; + d.status = SND_UMP_STREAM_MSG_STATUS_START_CLIP; + write_ump(file, &d); +} + +/* write an End Clip message */ +static void write_end_clip(FILE *file) +{ + snd_ump_msg_stream_gen_t d = {}; + + d.type = SND_UMP_MSG_TYPE_STREAM; + d.status = SND_UMP_STREAM_MSG_STATUS_END_CLIP; + write_ump(file, &d); +} + +/* write a Set Tempo message */ +static void write_tempo(FILE *file) +{ + snd_ump_msg_set_tempo_t d = {}; + + d.type = SND_UMP_MSG_TYPE_FLEX_DATA; + d.group = 0; + d.format = SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE; + d.addrs = SND_UMP_FLEX_DATA_MSG_ADDR_GROUP; + d.status_bank = SND_UMP_FLEX_DATA_MSG_BANK_SETUP; + d.status = SND_UMP_FLEX_DATA_MSG_STATUS_SET_TEMPO; + d.tempo = (unsigned int)(6000000000ULL / beats); + write_ump(file, &d); +} + +/* write a Set Time Signature message */ +static void write_time_sig(FILE *file) +{ + snd_ump_msg_set_time_sig_t d = {}; + + d.type = SND_UMP_MSG_TYPE_FLEX_DATA; + d.group = 0; + d.format = SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE; + d.addrs = SND_UMP_FLEX_DATA_MSG_ADDR_GROUP; + d.status_bank = SND_UMP_FLEX_DATA_MSG_BANK_SETUP; + d.status = SND_UMP_FLEX_DATA_MSG_STATUS_SET_TIME_SIGNATURE; + d.numerator = ts_num; + d.denominator = ts_div; + d.num_notes = 8; + write_ump(file, &d); +} + +/* record the delta time from the last event */ +static void delta_time(FILE *file, const snd_seq_ump_event_t *ev) +{ + int diff = ev->time.tick - last_tick; + + if (diff <= 0) + return; + write_dcs(file, diff); + last_tick = ev->time.tick; +} + +static void record_event(FILE *file, const snd_seq_ump_event_t *ev) +{ + /* ignore events without proper timestamps */ + if (ev->queue != queue || !snd_seq_ev_is_tick(ev) || + !snd_seq_ev_is_ump(ev)) + return; + + delta_time(file, ev); + write_ump(file, ev->ump); +} + +/* read a UMP raw (big-endian) packet, return the packet length in words */ +static int read_ump_raw(FILE *file, uint32_t *buf) +{ + uint32_t v; + int i, num; + + if (fread(buf, 4, 1, file) != 1) + return 0; + v = be32toh(*buf); + num = snd_ump_packet_length(snd_ump_msg_hdr_type(v)); + for (i = 1; i < num; i++) { + if (fread(buf + i, 4, 1, file) != 1) + return 0; + } + return num; +} + +/* read the profile UMP data and write to the configuration */ +static void write_profiles(FILE *file) +{ + FILE *fp; + uint32_t ump[4]; + int len; + + if (!profile_ump_file) + return; + + fp = fopen(profile_ump_file, "rb"); + if (!fp) + fatal("cannot open the profile '%s'", profile_ump_file); + + while (!feof(fp)) { + len = read_ump_raw(fp, ump); + if (!len) + break; + fwrite(ump, 4, len, file); + } + + fclose(fp); +} + +/* write Flex Data metadata text given by command lines */ +static void write_metadata(FILE *file, unsigned int type, const char *text) +{ + int len = strlen(text), size; + unsigned int format = SND_UMP_FLEX_DATA_MSG_FORMAT_START; + + while (len > 0) { + snd_ump_msg_flex_data_t d = {}; + + if (len <= 12) { + if (format == SND_UMP_FLEX_DATA_MSG_FORMAT_CONTINUE) + format = SND_UMP_FLEX_DATA_MSG_FORMAT_END; + else + format = SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE; + size = len; + } else { + size = 12; + } + + d.meta.type = SND_UMP_MSG_TYPE_FLEX_DATA; + d.meta.addrs = SND_UMP_FLEX_DATA_MSG_ADDR_GROUP; + d.meta.status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA; + d.meta.status = type; + d.meta.format = format; + + /* keep the data in big endian */ + d.raw[0] = htobe32(d.raw[0]); + /* strings are copied as-is in big-endian */ + memcpy(d.meta.data, text, size); + + fwrite(d.raw, 4, 4, file); + len -= size; + text += size; + format = SND_UMP_FLEX_DATA_MSG_FORMAT_CONTINUE; + } +} + +/* write MIDI Clip file header and the configuration packets */ +static void write_file_header(FILE *file) +{ + int i; + + /* header id */ + fwrite("SMF2CLIP", 1, 8, file); + + /* clip configuration header */ + write_profiles(file); + + for (i = 0; i < metadata_num; i++) + write_metadata(file, metadata_types[i], metadata_texts[i]); + + /* first DCS */ + write_dcs(file, 0); + write_dctpq(file); +} + +/* write start bar */ +static void start_bar(FILE *file) +{ + int err; + + /* start the queue */ + err = snd_seq_start_queue(seq, queue, NULL); + check_snd("start queue", err); + snd_seq_drain_output(seq); + + write_start_clip(file); + write_tempo(file); + write_time_sig(file); +} + +static void help(const char *argv0) +{ + fprintf(stderr, "Usage: %s [options] outputfile\n" + "\nAvailable options:\n" + " -h,--help this help\n" + " -V,--version show version\n" + " -p,--port=client:port,... source port(s)\n" + " -b,--bpm=beats tempo in beats per minute\n" + " -t,--ticks=ticks resolution in ticks per beat or frame\n" + " -i,--timesig=nn:dd time signature\n" + " -n,--num-events=events fixed number of events to record, then exit\n" + " -u,--ump=version UMP MIDI version (1 or 2)\n" + " -r,--interactive Interactive mode\n" + " -s,--silent don't print messages\n" + " -P,--profile=file configuration profile UMP\n" + " --project=text put project name meta data text\n" + " --song=text put song name meta data text\n" + " --clip=text put MIDI clip name meta data text\n" + " --copyright=text put copyright notice meta data text\n" + " --composer=text put composer name meta data text\n" + " --lyricist=text put lyricist name meta data text\n" + " --arranger=text put arranger name meta data text\n" + " --publisher=text put publisher name meta data text\n" + " --publisher=text put publisher name meta data text\n" + " --publisher=text put publisher name meta data text\n" + " --performer=text put performer name meta data text\n" + " --accompany=text put accompany performer name meta data text\n" + " --date=text put recording date meta data text\n" + " --location=text put recording location meta data text\n", + argv0); +} + +static void version(void) +{ + fputs("arecordmidi version " SND_UTIL_VERSION_STR "\n", stderr); +} + +static void sighandler(int sig ATTRIBUTE_UNUSED) +{ + stop = 1; +} + +#define OPT_META_BIT 0x1000 +enum { + OPT_META_PROJECT = 0x1001, + OPT_META_SONG = 0x1002, + OPT_META_CLIP = 0x1003, + OPT_META_COPYRIGHT = 0x1004, + OPT_META_COMPOSER = 0x1005, + OPT_META_LYRICIST = 0x1006, + OPT_META_ARRANGER = 0x1007, + OPT_META_PUBLISHER = 0x1008, + OPT_META_PERFORMER = 0x1009, + OPT_META_ACCOMPANY = 0x100a, + OPT_META_DATE = 0x100b, + OPT_META_LOCATION = 0x100c, +}; + +int main(int argc, char *argv[]) +{ + static const char short_options[] = "hVp:b:t:n:u:rsP:"; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"port", 1, NULL, 'p'}, + {"bpm", 1, NULL, 'b'}, + {"ticks", 1, NULL, 't'}, + {"timesig", 1, NULL, 'i'}, + {"num-events", 1, NULL, 'n'}, + {"ump", 1, NULL, 'u'}, + {"interactive", 0, NULL, 'r'}, + {"silent", 0, NULL, 's'}, + {"profile", 1, NULL, 'P'}, + /* meta data texts */ + {"project", 1, NULL, OPT_META_PROJECT}, + {"song", 1, NULL, OPT_META_SONG}, + {"clip", 1, NULL, OPT_META_CLIP}, + {"copyright", 1, NULL, OPT_META_COPYRIGHT}, + {"composer", 1, NULL, OPT_META_COMPOSER}, + {"lyricist", 1, NULL, OPT_META_LYRICIST}, + {"arranger", 1, NULL, OPT_META_ARRANGER}, + {"publisher", 1, NULL, OPT_META_PUBLISHER}, + {"performer", 1, NULL, OPT_META_PERFORMER}, + {"accompany", 1, NULL, OPT_META_ACCOMPANY}, + {"date", 1, NULL, OPT_META_DATE}, + {"location", 1, NULL, OPT_META_LOCATION}, + {0} + }; + + char *filename; + FILE *file; + struct pollfd *pfds; + int npfds; + int c, err; + /* If |num_events| isn't specified, leave it at 0. */ + long num_events = 0; + long events_received = 0; + int start = 0; + int interactive = 0; + + init_seq(); + + while ((c = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (c) { + case 'h': + help(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'p': + parse_ports(optarg); + break; + case 'b': + beats = atoi(optarg); + if (beats < 4 || beats > 6000) + fatal("Invalid tempo"); + break; + case 't': + ticks = atoi(optarg); + if (ticks < 1 || ticks > 0x7fff) + fatal("Invalid number of ticks"); + break; + case 'i': + time_signature(optarg); + break; + case 'n': + err = 0; + num_events = arg_parse_decimal_num(optarg, &err); + if (err != 0) { + fatal("Couldn't parse num_events argument: %s\n", + strerror(-err)); + } + if (num_events <= 0) + fatal("num_events must be greater than 0"); + break; + case 'u': + midi_version = atoi(optarg); + if (midi_version != 1 && midi_version != 2) + fatal("Invalid MIDI version %d\n", midi_version); + break; + case 'r': + interactive = 1; + break; + case 's': + silent = 1; + break; + case 'P': + profile_ump_file = optarg; + break; + default: + if (c & OPT_META_BIT) { + if (metadata_num >= MAX_METADATA) + fatal("Too many metadata given"); + metadata_types[metadata_num] = c & 0x0f; + metadata_texts[metadata_num] = optarg; + metadata_num++; + break; + } + help(argv[0]); + return 1; + } + } + + if (optind >= argc) { + fputs("Please specify a file to record to.\n", stderr); + return 1; + } + + create_queue(); + create_ump_client(); + if (port_count) + connect_ports(); + + filename = argv[optind]; + + if (!strcmp(filename, "-")) { + file = stdout; + silent = 1; // imply silent mode + } else { + file = fopen(filename, "wb"); + if (!file) + fatal("Cannot open %s - %s", filename, strerror(errno)); + } + + write_file_header(file); + if (interactive) { + if (!silent) { + printf("Press RETURN to start recording:"); + fflush(stdout); + } + } else { + start_bar(file); + start = 1; + } + + err = snd_seq_nonblock(seq, 1); + check_snd("set nonblock mode", err); + + signal(SIGINT, sighandler); + signal(SIGTERM, sighandler); + + npfds = snd_seq_poll_descriptors_count(seq, POLLIN); + pfds = alloca(sizeof(*pfds) * (npfds + 1)); + for (;;) { + snd_seq_poll_descriptors(seq, pfds, npfds, POLLIN); + if (interactive) { + pfds[npfds].fd = STDIN_FILENO; + pfds[npfds].events = POLLIN | POLLERR | POLLNVAL; + if (poll(pfds, npfds + 1, -1) < 0) + break; + if (pfds[npfds].revents & POLLIN) { + while (!feof(stdin) && getchar() != '\n') + ; + if (!start) { + start_bar(file); + start = 1; + if (!silent) { + printf("Press RETURN to stop recording:"); + fflush(stdout); + } + continue; + } else { + stop = 1; + } + } + } else { + if (poll(pfds, npfds, -1) < 0) + break; + } + + do { + snd_seq_ump_event_t *event; + + err = snd_seq_ump_event_input(seq, &event); + if (err < 0) + break; + if (start && event) { + record_event(file, event); + events_received++; + } + } while (err > 0); + if (stop) + break; + if (num_events && (events_received >= num_events)) + break; + } + + if (num_events && events_received < num_events) { + if (!silent) + fputs("Warning: Received signal before num_events\n", stdout); + } + + write_end_clip(file); + if (file != stdout) + fclose(file); + snd_seq_close(seq); + return 0; +} diff --git a/seq/aseqdump/Makefile.am b/seq/aseqdump/Makefile.am new file mode 100644 index 0000000..4837ad9 --- /dev/null +++ b/seq/aseqdump/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = aseqdump.1 + +bin_PROGRAMS = aseqdump +man_MANS = aseqdump.1 diff --git a/seq/aseqdump/aseqdump.1 b/seq/aseqdump/aseqdump.1 new file mode 100644 index 0000000..7b389bd --- /dev/null +++ b/seq/aseqdump/aseqdump.1 @@ -0,0 +1,73 @@ +.TH ASEQDUMP 1 "19 Feb 2005" + +.SH NAME +aseqdump \- show the events received at an ALSA sequencer port + +.SH SYNOPSIS +.B aseqdump +[\fI\-p client:port,...\fP] + +.SH DESCRIPTION +.B aseqdump +is a command-line utility that prints the sequencer events it receives as text. + +To stop receiving, press Ctrl+C. + +.SH OPTIONS + +.TP +.I \-h,\-\-help +Prints a list of options. + +.TP +.I \-V,\-\-version +Prints the current version. + +.TP +.I \-l,\-\-list +Prints a list of possible input ports. + +.TP +.I \-u,\-\-ump=version +Sets the client MIDI version. +0 is for legacy mode, 1 is UMP MIDI 1.0 mode, and 2 is UMP MIDI 2.0 mode. + +.TP +.I \-r,\-\-raw +Suppress the automatic conversion of events among UMP and legacy clients. + +.TP +.I \-R,\-\-raw-view +Shows the raw values as is. +The channel numbers and UMP group numbers are 0-based in this mode. +This is the default behavior. + +.TP +.I \-N,\-\-normalized-view +Shows the normalized values to be aligned with MIDI 1.0. +The channel numbers and UMP group numbers are shown as 1-based values, +i.e. 1 is the lowest number. +The velocity and data values are normalized between 0 and 127. +The values for MIDI 2.0 are shown with two digits decimal points (from +0.00 to 127.00). +The pitch wheel is normalized between -8192 to 8192, too. + +.TP +.I \-P,\-\-percent-view +Shows the values in percentage. +The channel numbers and UMP group numbers are shown as 1-based values +in this mode, too. +The velocity and data values are normalized between 0 and 100%, shown +with two digits decimal points. +The pitch wheel is normalized between -100% to 100%, too. + +.TP +.I \-p,\-\-port=client:port,... +Sets the sequencer port(s) from which events are received. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +.SH AUTHOR +Clemens Ladisch diff --git a/seq/aseqdump/aseqdump.c b/seq/aseqdump/aseqdump.c new file mode 100644 index 0000000..7f14b8f --- /dev/null +++ b/seq/aseqdump/aseqdump.c @@ -0,0 +1,1233 @@ +/* + * aseqdump.c - show the events received at an ALSA sequencer port + * + * Copyright (c) 2005 Clemens Ladisch + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "version.h" +#include + +enum { + VIEW_RAW, VIEW_NORMALIZED, VIEW_PERCENT +}; + +static snd_seq_t *seq; +static int port_count; +static snd_seq_addr_t *ports; +static volatile sig_atomic_t stop = 0; +static int ump_version; +static int view_mode = VIEW_RAW; + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +/* memory allocation error handling */ +static void check_mem(void *p) +{ + if (!p) + fatal("Out of memory"); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + /* set our client's name */ + err = snd_seq_set_client_name(seq, "aseqdump"); + check_snd("set client name", err); +} + +/* parses one or more port addresses from the string */ +static void parse_ports(const char *arg) +{ + char *buf, *s, *port_name; + int err; + + /* make a copy of the string because we're going to modify it */ + buf = strdup(arg); + check_mem(buf); + + for (port_name = s = buf; s; port_name = s + 1) { + /* Assume that ports are separated by commas. We don't use + * spaces because those are valid in client names. */ + s = strchr(port_name, ','); + if (s) + *s = '\0'; + + ++port_count; + ports = realloc(ports, port_count * sizeof(snd_seq_addr_t)); + check_mem(ports); + + err = snd_seq_parse_address(seq, &ports[port_count - 1], port_name); + if (err < 0) + fatal("Invalid port %s - %s", port_name, snd_strerror(err)); + } + + free(buf); +} + +static void create_port(void) +{ + int err; + + err = snd_seq_create_simple_port(seq, "aseqdump", + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + check_snd("create port", err); +} + +static void connect_ports(void) +{ + int i, err; + + for (i = 0; i < port_count; ++i) { + err = snd_seq_connect_from(seq, 0, ports[i].client, ports[i].port); + if (err < 0) + fatal("Cannot connect from port %d:%d - %s", + ports[i].client, ports[i].port, snd_strerror(err)); + } +} + +static int channel_number(unsigned char c) +{ + if (view_mode != VIEW_RAW) + return c + 1; + else + return c; +} + +static const char *midi1_data(unsigned int v) +{ + static char tmp[32]; + + if (view_mode == VIEW_PERCENT) { + if (v <= 64) + snprintf(tmp, sizeof(tmp), "%.2f%%", + ((double)v * 50.0) / 64); + else + snprintf(tmp, sizeof(tmp), "%.2f%%", + ((double)(v - 64) * 50.0) / 63 + 50.0); + return tmp; + } + + sprintf(tmp, "%d", v); + return tmp; +} + +static const char *midi1_pitchbend(int v) +{ + static char tmp[32]; + + if (view_mode == VIEW_PERCENT) { + if (v < 0) + snprintf(tmp, sizeof(tmp), "%.2f%%", + ((double)v * 100.0) / 8192); + else + snprintf(tmp, sizeof(tmp), "%.2f%%", + ((double)v * 100.0) / 8191); + return tmp; + } + + sprintf(tmp, "%d", v); + return tmp; +} + +static void dump_event(const snd_seq_event_t *ev) +{ + printf("%3d:%-3d ", ev->source.client, ev->source.port); + + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: + if (ev->data.note.velocity) + printf("Note on %2d, note %d, velocity %s\n", + channel_number(ev->data.note.channel), + ev->data.note.note, + midi1_data(ev->data.note.velocity)); + else + printf("Note off %2d, note %d\n", + channel_number(ev->data.note.channel), + ev->data.note.note); + break; + case SND_SEQ_EVENT_NOTEOFF: + printf("Note off %2d, note %d, velocity %s\n", + channel_number(ev->data.note.channel), + ev->data.note.note, + midi1_data(ev->data.note.velocity)); + break; + case SND_SEQ_EVENT_KEYPRESS: + printf("Polyphonic aftertouch %2d, note %d, value %s\n", + channel_number(ev->data.note.channel), + ev->data.note.note, + midi1_data(ev->data.note.velocity)); + break; + case SND_SEQ_EVENT_CONTROLLER: + printf("Control change %2d, controller %d, value %d\n", + channel_number(ev->data.control.channel), + ev->data.control.param, ev->data.control.value); + break; + case SND_SEQ_EVENT_PGMCHANGE: + printf("Program change %2d, program %d\n", + channel_number(ev->data.control.channel), + ev->data.control.value); + break; + case SND_SEQ_EVENT_CHANPRESS: + printf("Channel aftertouch %2d, value %s\n", + channel_number(ev->data.control.channel), + midi1_data(ev->data.control.value)); + break; + case SND_SEQ_EVENT_PITCHBEND: + printf("Pitch bend %2d, value %s\n", + channel_number(ev->data.control.channel), + midi1_pitchbend(ev->data.control.value)); + break; + case SND_SEQ_EVENT_CONTROL14: + printf("Control change %2d, controller %d, value %5d\n", + channel_number(ev->data.control.channel), + ev->data.control.param, ev->data.control.value); + break; + case SND_SEQ_EVENT_NONREGPARAM: + printf("Non-reg. parameter %2d, parameter %d, value %d\n", + channel_number(ev->data.control.channel), + ev->data.control.param, ev->data.control.value); + break; + case SND_SEQ_EVENT_REGPARAM: + printf("Reg. parameter %2d, parameter %d, value %d\n", + channel_number(ev->data.control.channel), + ev->data.control.param, ev->data.control.value); + break; + case SND_SEQ_EVENT_SONGPOS: + printf("Song position pointer value %d\n", + ev->data.control.value); + break; + case SND_SEQ_EVENT_SONGSEL: + printf("Song select value %d\n", + ev->data.control.value); + break; + case SND_SEQ_EVENT_QFRAME: + printf("MTC quarter frame %02xh\n", + ev->data.control.value); + break; + case SND_SEQ_EVENT_TIMESIGN: + // XXX how is this encoded? + printf("SMF time signature (%#010x)\n", + ev->data.control.value); + break; + case SND_SEQ_EVENT_KEYSIGN: + // XXX how is this encoded? + printf("SMF key signature (%#010x)\n", + ev->data.control.value); + break; + case SND_SEQ_EVENT_START: + if (ev->source.client == SND_SEQ_CLIENT_SYSTEM && + ev->source.port == SND_SEQ_PORT_SYSTEM_TIMER) + printf("Queue start queue %d\n", + ev->data.queue.queue); + else + printf("Start\n"); + break; + case SND_SEQ_EVENT_CONTINUE: + if (ev->source.client == SND_SEQ_CLIENT_SYSTEM && + ev->source.port == SND_SEQ_PORT_SYSTEM_TIMER) + printf("Queue continue queue %d\n", + ev->data.queue.queue); + else + printf("Continue\n"); + break; + case SND_SEQ_EVENT_STOP: + if (ev->source.client == SND_SEQ_CLIENT_SYSTEM && + ev->source.port == SND_SEQ_PORT_SYSTEM_TIMER) + printf("Queue stop queue %d\n", + ev->data.queue.queue); + else + printf("Stop\n"); + break; + case SND_SEQ_EVENT_SETPOS_TICK: + printf("Set tick queue pos. queue %d\n", ev->data.queue.queue); + break; + case SND_SEQ_EVENT_SETPOS_TIME: + printf("Set rt queue pos. queue %d\n", ev->data.queue.queue); + break; + case SND_SEQ_EVENT_TEMPO: + printf("Set queue tempo queue %d\n", ev->data.queue.queue); + break; + case SND_SEQ_EVENT_CLOCK: + printf("Clock\n"); + break; + case SND_SEQ_EVENT_TICK: + printf("Tick\n"); + break; + case SND_SEQ_EVENT_QUEUE_SKEW: + printf("Queue timer skew queue %d\n", ev->data.queue.queue); + break; + case SND_SEQ_EVENT_TUNE_REQUEST: + printf("Tune request\n"); + break; + case SND_SEQ_EVENT_RESET: + printf("Reset\n"); + break; + case SND_SEQ_EVENT_SENSING: + printf("Active Sensing\n"); + break; + case SND_SEQ_EVENT_CLIENT_START: + printf("Client start client %d\n", + ev->data.addr.client); + break; + case SND_SEQ_EVENT_CLIENT_EXIT: + printf("Client exit client %d\n", + ev->data.addr.client); + break; + case SND_SEQ_EVENT_CLIENT_CHANGE: + printf("Client changed client %d\n", + ev->data.addr.client); + break; + case SND_SEQ_EVENT_PORT_START: + printf("Port start %d:%d\n", + ev->data.addr.client, ev->data.addr.port); + break; + case SND_SEQ_EVENT_PORT_EXIT: + printf("Port exit %d:%d\n", + ev->data.addr.client, ev->data.addr.port); + break; + case SND_SEQ_EVENT_PORT_CHANGE: + printf("Port changed %d:%d\n", + ev->data.addr.client, ev->data.addr.port); + break; + case SND_SEQ_EVENT_PORT_SUBSCRIBED: + printf("Port subscribed %d:%d -> %d:%d\n", + ev->data.connect.sender.client, ev->data.connect.sender.port, + ev->data.connect.dest.client, ev->data.connect.dest.port); + break; + case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: + printf("Port unsubscribed %d:%d -> %d:%d\n", + ev->data.connect.sender.client, ev->data.connect.sender.port, + ev->data.connect.dest.client, ev->data.connect.dest.port); + break; + case SND_SEQ_EVENT_SYSEX: + { + unsigned int i; + printf("System exclusive "); + for (i = 0; i < ev->data.ext.len; ++i) + printf(" %02X", ((unsigned char*)ev->data.ext.ptr)[i]); + printf("\n"); + } + break; + default: + printf("Event type %d\n", ev->type); + } +} + +static int group_number(unsigned char c) +{ + if (view_mode != VIEW_RAW) + return c + 1; + else + return c; +} + +static const char *pitchbend_value(uint8_t msb, uint8_t lsb) +{ + int pb = (msb << 7) | lsb; + + return midi1_pitchbend(pb - 8192); +} + +static void dump_ump_midi1_event(const unsigned int *ump) +{ + const snd_ump_msg_midi1_t *m = (const snd_ump_msg_midi1_t *)ump; + unsigned char group = group_number(m->hdr.group); + unsigned char status = m->hdr.status; + unsigned char channel = channel_number(m->hdr.channel); + + printf("Group %2d, ", group); + switch (status) { + case SND_UMP_MSG_NOTE_OFF: + printf("Note off %2d, note %d, velocity %s", + channel, m->note_off.note, + midi1_data(m->note_off.velocity)); + break; + case SND_UMP_MSG_NOTE_ON: + printf("Note on %2d, note %d, velocity %s", + channel, m->note_on.note, + midi1_data(m->note_on.velocity)); + break; + case SND_UMP_MSG_POLY_PRESSURE: + printf("Poly pressure %2d, note %d, value %s", + channel, m->poly_pressure.note, + midi1_data(m->poly_pressure.data)); + break; + case SND_UMP_MSG_CONTROL_CHANGE: + printf("Control change %2d, controller %d, value %d", + channel, m->control_change.index, m->control_change.data); + break; + case SND_UMP_MSG_PROGRAM_CHANGE: + printf("Program change %2d, program %d", + channel, m->program_change.program); + break; + case SND_UMP_MSG_CHANNEL_PRESSURE: + printf("Channel pressure %2d, value %s", + channel, midi1_data(m->channel_pressure.data)); + break; + case SND_UMP_MSG_PITCHBEND: + printf("Pitchbend %2d, value %s", + channel, pitchbend_value(m->pitchbend.data_msb, + m->pitchbend.data_lsb)); + break; + default: + printf("UMP MIDI1 event: status = %d, channel = %d, 0x%08x", + status, channel, *ump); + break; + } + printf("\n"); +} + +static const char *midi2_velocity(unsigned int v) +{ + static char tmp[32]; + + if (view_mode == VIEW_NORMALIZED) { + if (v <= 0x8000) + snprintf(tmp, sizeof(tmp), "%.2f", + ((double)v * 64.0) / 0x8000); + else + snprintf(tmp, sizeof(tmp), "%.2f", + ((double)(v - 0x8000) * 63.0) / 0x7fff + 64.0); + return tmp; + } else if (view_mode == VIEW_PERCENT) { + snprintf(tmp, sizeof(tmp), "%.2f%%", ((double)v * 100.0) / 0xffff); + return tmp; + } + + sprintf(tmp, "0x%x", v); + return tmp; +} + +static const char *midi2_data(unsigned int v) +{ + static char tmp[32]; + + if (view_mode == VIEW_NORMALIZED) { + if (!v) + return "0"; + else if (v == 0xffffffffU) + return "127"; + if (v <= 0x80000000) + snprintf(tmp, sizeof(tmp), "%.2f", + ((double)v * 64.0) / 0x80000000U); + else + snprintf(tmp, sizeof(tmp), "%.2f", + ((double)(v - 0x80000000U) * 63.0) / 0x7fffffffU + 64.0); + return tmp; + } else if (view_mode == VIEW_PERCENT) { + snprintf(tmp, sizeof(tmp), "%.2f%%", ((double)v * 100.0) / 0xffffffffU); + return tmp; + } + + sprintf(tmp, "0x%x", v); + return tmp; +} + +static const char *midi2_pitchbend(unsigned int v) +{ + static char tmp[32]; + + if (view_mode == VIEW_NORMALIZED) { + if (!v) + return "-8192"; + else if (v == 0xffffffffU) + return "8191"; + if (v <= 0x80000000) + snprintf(tmp, sizeof(tmp), "%.2f", + ((int)(v ^ 0x80000000U) * 8192.0) / 0x80000000U); + else + snprintf(tmp, sizeof(tmp), "%.2f", + ((double)(v - 0x80000000U) * 8191.0) / 0x7fffffffU + 8192.0); + return tmp; + } else if (view_mode == VIEW_PERCENT) { + snprintf(tmp, sizeof(tmp), "%.2f%%", ((int)(v ^ 0x80000000U) * 100.0) / 0xffffffffU); + return tmp; + } + + sprintf(tmp, "0x%x", v); + return tmp; +} + +static void dump_ump_midi2_event(const unsigned int *ump) +{ + const snd_ump_msg_midi2_t *m = (const snd_ump_msg_midi2_t *)ump; + unsigned char group = group_number(m->hdr.group); + unsigned char status = m->hdr.status; + unsigned char channel = channel_number(m->hdr.channel); + + printf("Group %2d, ", group); + switch (status) { + case SND_UMP_MSG_PER_NOTE_RCC: + printf("Per-note RCC %2d, note %u, index %u, value 0x%x", + channel, m->per_note_rcc.note, + m->per_note_rcc.index, m->per_note_rcc.data); + break; + case SND_UMP_MSG_PER_NOTE_ACC: + printf("Per-note ACC %2d, note %u, index %u, value 0x%x", + channel, m->per_note_acc.note, + m->per_note_acc.index, m->per_note_acc.data); + break; + case SND_UMP_MSG_RPN: + printf("RPN %2d, bank %u:%u, value 0x%x", + channel, m->rpn.bank, m->rpn.index, m->rpn.data); + break; + case SND_UMP_MSG_NRPN: + printf("NRPN %2d, bank %u:%u, value 0x%x", + channel, m->rpn.bank, m->rpn.index, m->rpn.data); + break; + case SND_UMP_MSG_RELATIVE_RPN: + printf("relative RPN %2d, bank %u:%u, value 0x%x", + channel, m->rpn.bank, m->rpn.index, m->rpn.data); + break; + case SND_UMP_MSG_RELATIVE_NRPN: + printf("relative NRP %2d, bank %u:%u, value 0x%x", + channel, m->rpn.bank, m->rpn.index, m->rpn.data); + break; + case SND_UMP_MSG_PER_NOTE_PITCHBEND: + printf("Per-note pitchbend %2d, note %d, value %s", + channel, m->per_note_pitchbend.note, + midi2_pitchbend(m->per_note_pitchbend.data)); + break; + case SND_UMP_MSG_NOTE_OFF: + printf("Note off %2d, note %d, velocity %s, attr type = %d, data = 0x%x", + channel, m->note_off.note, + midi2_velocity(m->note_off.velocity), + m->note_off.attr_type, m->note_off.attr_data); + break; + case SND_UMP_MSG_NOTE_ON: + printf("Note on %2d, note %d, velocity %s, attr type = %d, data = 0x%x", + channel, m->note_on.note, + midi2_velocity(m->note_on.velocity), + m->note_on.attr_type, m->note_on.attr_data); + break; + case SND_UMP_MSG_POLY_PRESSURE: + printf("Poly pressure %2d, note %d, value %s", + channel, m->poly_pressure.note, + midi2_data(m->poly_pressure.data)); + break; + case SND_UMP_MSG_CONTROL_CHANGE: + printf("Control change %2d, controller %d, value 0x%x", + channel, m->control_change.index, m->control_change.data); + break; + case SND_UMP_MSG_PROGRAM_CHANGE: + printf("Program change %2d, program %d", + channel, m->program_change.program); + if (m->program_change.bank_valid) + printf(", Bank select %d:%d", + m->program_change.bank_msb, + m->program_change.bank_lsb); + break; + case SND_UMP_MSG_CHANNEL_PRESSURE: + printf("Channel pressure %2d, value %s", + channel, + midi2_data(m->channel_pressure.data)); + break; + case SND_UMP_MSG_PITCHBEND: + printf("Pitchbend %2d, value %s", + channel, + midi2_pitchbend(m->channel_pressure.data)); + break; + case SND_UMP_MSG_PER_NOTE_MGMT: + printf("Per-note management %2d, value 0x%x", + channel, m->per_note_mgmt.flags); + break; + default: + printf("UMP MIDI2 event: status = %d, channel = %d, 0x%08x", + status, channel, *ump); + break; + } + printf("\n"); +} + +static void dump_ump_utility_event(const unsigned int *ump) +{ + unsigned char status = snd_ump_msg_status(ump); + unsigned int val = *ump & 0xfffff; + + printf(" "); + switch (status) { + case SND_UMP_UTILITY_MSG_STATUS_NOOP: + printf("Noop\n"); + break; + case SND_UMP_UTILITY_MSG_STATUS_JR_CLOCK: + printf("JR Clock value %d\n", val); + break; + case SND_UMP_UTILITY_MSG_STATUS_JR_TSTAMP: + printf("JR Timestamp value %d\n", val); + break; + case SND_UMP_UTILITY_MSG_STATUS_DCTPQ: + printf("DCTPQ value %d\n", val); + break; + case SND_UMP_UTILITY_MSG_STATUS_DC: + printf("DC Ticks value %d\n", val); + break; + default: + printf("UMP Utility event: status = %d, 0x%08x\n", + status, *ump); + break; + } +} + +static void dump_ump_system_event(const unsigned int *ump) +{ + const snd_ump_msg_system_t *m = (const snd_ump_msg_system_t *)ump; + + printf("Group %2d, ", group_number(m->group)); + switch (m->status) { + case SND_UMP_MSG_MIDI_TIME_CODE: + printf("MIDI Time Code value %d\n", m->parm1); + break; + case SND_UMP_MSG_SONG_POSITION: + printf("Song position pointer value %d\n", + ((unsigned int)m->parm2 << 7) | m->parm1); + break; + case SND_UMP_MSG_SONG_SELECT: + printf("Song select value %d\n", m->parm1); + break; + case SND_UMP_MSG_TUNE_REQUEST: + printf("Tune request\n"); + break; + case SND_UMP_MSG_TIMING_CLOCK: + printf("Timing clock\n"); + break; + case SND_UMP_MSG_START: + printf("Start\n"); + break; + case SND_UMP_MSG_CONTINUE: + printf("Continue\n"); + break; + case SND_UMP_MSG_STOP: + printf("Stop\n"); + break; + case SND_UMP_MSG_ACTIVE_SENSING: + printf("Active sensing\n"); + break; + case SND_UMP_MSG_RESET: + printf("Reset\n"); + break; + default: + printf("UMP System event: status = %d, 0x%08x\n", + m->status, *ump); + break; + } +} + +static unsigned char ump_sysex7_data(const unsigned int *ump, + unsigned int offset) +{ + return snd_ump_get_byte(ump, offset + 2); +} + +static void dump_ump_sysex_status(const char *prefix, unsigned int status) +{ + printf("%s ", prefix); + switch (status) { + case SND_UMP_SYSEX_STATUS_SINGLE: + printf("Single "); + break; + case SND_UMP_SYSEX_STATUS_START: + printf("Start "); + break; + case SND_UMP_SYSEX_STATUS_CONTINUE: + printf("Continue"); + break; + case SND_UMP_SYSEX_STATUS_END: + printf("End "); + break; + default: + printf("(0x%04x)", status); + break; + } +} + +static void dump_ump_sysex_event(const unsigned int *ump) +{ + int i, length; + + printf("Group %2d, ", group_number(snd_ump_msg_group(ump))); + dump_ump_sysex_status("SysEx", snd_ump_sysex_msg_status(ump)); + length = snd_ump_sysex_msg_length(ump); + printf(" length %d ", length); + if (length > 6) + length = 6; + for (i = 0; i < length; i++) + printf("%s%02x", i ? ":" : "", ump_sysex7_data(ump, i)); + printf("\n"); +} + +static unsigned char ump_sysex8_data(const unsigned int *ump, + unsigned int offset) +{ + return snd_ump_get_byte(ump, offset + 3); +} + +static void dump_ump_sysex8_event(const unsigned int *ump) +{ + int i, length; + + printf("Group %2d, ", group_number(snd_ump_msg_group(ump))); + dump_ump_sysex_status("SysEx8", snd_ump_sysex_msg_status(ump)); + length = snd_ump_sysex_msg_length(ump); + printf(" length %d ", length); + printf(" stream %d ", (ump[0] >> 8) & 0xff); + if (length > 13) + length = 13; + for (i = 0; i < length; i++) + printf("%s%02x", i ? ":" : "", ump_sysex8_data(ump, i)); + printf("\n"); +} + +static void dump_ump_mixed_data_event(const unsigned int *ump) +{ + const snd_ump_msg_mixed_data_t *m = + (const snd_ump_msg_mixed_data_t *)ump; + int i; + + printf("Group %2d, ", group_number(snd_ump_msg_group(ump))); + switch (snd_ump_sysex_msg_status(ump)) { + case SND_UMP_MIXED_DATA_SET_STATUS_HEADER: + printf("MDS Header id=0x%x, bytes=%d, chunk=%d/%d, manufacturer=0x%04x, device=0x%04x, sub_id=0x%04x 0x%04x\n", + m->header.mds_id, m->header.bytes, + m->header.chunk, m->header.chunks, + m->header.manufacturer, m->header.device, + m->header.sub_id_1, m->header.sub_id_2); + break; + case SND_UMP_MIXED_DATA_SET_STATUS_PAYLOAD: + printf("MDS Payload id=0x%x, ", m->payload.mds_id); + for (i = 0; i < 14; i++) + printf("%s%02x", i ? ":" : "", + snd_ump_get_byte(ump, i + 2)); + printf("\n"); + break; + default: + printf("Extended Data (status 0x%x)\n", + snd_ump_sysex_msg_status(ump)); + break; + } +} + +static void dump_ump_extended_data_event(const unsigned int *ump) +{ + unsigned char status = snd_ump_sysex_msg_status(ump); + + if (status < 4) + dump_ump_sysex8_event(ump); + else + dump_ump_mixed_data_event(ump); +} + +static void print_ump_string(const unsigned int *ump, unsigned int fmt, + unsigned int offset, int maxlen) +{ + static const char *fmtstr[4] = { "Single", "Start", "Cont", "End" }; + unsigned char buf[32]; + int i = 0; + + do { + buf[i] = snd_ump_get_byte(ump, offset); + if (!buf[i]) + break; + if (buf[i] < 0x20) + buf[i] = '.'; + offset++; + } while (++i < maxlen); + buf[i] = 0; + + printf("%6s: %s", fmtstr[fmt], buf); +} + +static void dump_ump_stream_event(const unsigned int *ump) +{ + const snd_ump_msg_stream_t *s = (const snd_ump_msg_stream_t *)ump; + + printf(" "); /* stream message is groupless */ + switch (s->gen.status) { + case SND_UMP_STREAM_MSG_STATUS_EP_DISCOVERY: + printf("EP Discovery ver=%d/%d, filter=0x%x\n", + (ump[0] >> 8) & 0xff, ump[0] & 0xff, ump[1] & 0xff); + break; + case SND_UMP_STREAM_MSG_STATUS_EP_INFO: + printf("EP Info ver=%d/%d, static=%d, fb#=%d, midi2=%d, midi1=%d, rxjr=%d, txjr=%d\n", + (ump[0] >> 8) & 0xff, ump[0] & 0xff, (ump[1] >> 31), + (ump[1] >> 24) & 0x7f, + (ump[1] >> 9) & 1, (ump[1] >> 8) & 1, + (ump[1] >> 1) & 1, ump[1] & 1); + break; + case SND_UMP_STREAM_MSG_STATUS_DEVICE_INFO: + printf("Device Info sysid=%02x:%02x:%02x, family=%02x:%02x, model=%02x:%02x, rev=%02x:%02x:%02x:%02x\n", + (ump[1] >> 16) & 0x7f, (ump[1] >> 8) & 0x7f, ump[1] & 0x7f, + (ump[2] >> 16) & 0x7f, (ump[2] >> 24) & 0x7f, + ump[2] & 0x7f, (ump[2] >> 8) & 0x7f, + (ump[3] >> 24) & 0x7f, (ump[3] >> 16) & 0x7f, + (ump[3] >> 8) & 0x7f, ump[3] & 0x7f); + break; + case SND_UMP_STREAM_MSG_STATUS_EP_NAME: + printf("EP Name "); + print_ump_string(ump, (ump[0] >> 26) & 3, 2, 14); + printf("\n"); + break; + case SND_UMP_STREAM_MSG_STATUS_PRODUCT_ID: + printf("Product Id "); + print_ump_string(ump, (ump[0] >> 26) & 3, 2, 14); + printf("\n"); + break; + case SND_UMP_STREAM_MSG_STATUS_STREAM_CFG_REQUEST: + printf("Stream Cfg Req protocl=%d, rxjr=%d, txjr=%d\n", + (ump[0] >> 8) & 0xff, (ump[0] >> 1) & 1, ump[0] & 1); + break; + case SND_UMP_STREAM_MSG_STATUS_STREAM_CFG: + printf("Stream Cfg protocl=%d, rxjr=%d, txjr=%d\n", + (ump[0] >> 8) & 0xff, (ump[0] >> 1) & 1, ump[0] & 1); + break; + case SND_UMP_STREAM_MSG_STATUS_FB_DISCOVERY: + printf("FB Discovery fb#=%d, filter=0x%x\n", + (ump[0] >> 8) & 0xff, ump[0] & 0xff); + break; + case SND_UMP_STREAM_MSG_STATUS_FB_INFO: + printf("FB Info fb#=%d, active=%d, ui=%d, MIDI1=%d, dir=%d, group=%d-%d, MIDI-CI=%d, SysEx8=%d\n", + (ump[0] >> 8) & 0x7f, (ump[0] >> 15) & 1, + (ump[0] >> 4) & 3, (ump[0] >> 2) & 3, ump[0] & 3, + (ump[1] >> 24) & 0xff, (ump[1] >> 16) & 0xff, + (ump[1] >> 8) * 0xff, ump[1] & 0xff); + break; + case SND_UMP_STREAM_MSG_STATUS_FB_NAME: + printf("Product Id "); + printf("FB Name #%02d ", (ump[0] >> 8) & 0xff); + print_ump_string(ump, (ump[0] >> 26) & 3, 3, 13); + printf("\n"); + break; + case SND_UMP_STREAM_MSG_STATUS_START_CLIP: + printf("Start Clip\n"); + break; + case SND_UMP_STREAM_MSG_STATUS_END_CLIP: + printf("End Clip\n"); + break; + default: + printf("UMP Stream event: status = %d, 0x%08x:0x%08x:0x%08x:0x%08x\n", + s->gen.status, ump[0], ump[1], ump[2], ump[3]); + break; + } +} + +struct flexdata_text_prefix { + unsigned char status_bank; + unsigned char status; + const char *prefix; +}; + +static struct flexdata_text_prefix text_prefix[] = { + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_PROJECT_NAME, + .prefix = "Project" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_SONG_NAME, + .prefix = "Song" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_MIDI_CLIP_NAME, + .prefix = "MIDI Clip" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_COPYRIGHT_NOTICE, + .prefix = "Copyright" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_COMPOSER_NAME, + .prefix = "Composer" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_LYRICIST_NAME, + .prefix = "Lyricist" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_ARRANGER_NAME, + .prefix = "Arranger" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_PUBLISHER_NAME, + .prefix = "Publisher" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_PRIMARY_PERFORMER, + .prefix = "Performer" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_ACCOMPANY_PERFORMAER, + .prefix = "Accompany Performer" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RECORDING_DATE, + .prefix = "Recording Date" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_METADATA, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RECORDING_LOCATION, + .prefix = "Recording Location" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_LYRICS, + .prefix = "Lyrics" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_LYRICS_LANGUAGE, + .prefix = "Lyrics Language" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RUBY, + .prefix = "Ruby" }, + { .status_bank = SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT, + .status = SND_UMP_FLEX_DATA_MSG_STATUS_RUBY_LANGUAGE, + .prefix = "Ruby Language" }, + {} +}; + +static const char *ump_meta_prefix(const snd_ump_msg_flex_data_t *fh) +{ + static char buf[32]; + int i; + + for (i = 0; text_prefix[i].status_bank; i++) { + if (text_prefix[i].status_bank == fh->meta.status_bank && + text_prefix[i].status == fh->meta.status) + return text_prefix[i].prefix; + } + + sprintf(buf, "(%d:%d)", fh->meta.status_bank, fh->meta.status); + return buf; +} + +static void dump_ump_flex_data_event(const unsigned int *ump) +{ + const snd_ump_msg_flex_data_t *fh = + (const snd_ump_msg_flex_data_t *)ump; + + printf("Group %2d, ", group_number(snd_ump_msg_group(ump))); + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_TEMPO) { + printf("UMP Set Tempo value %d\n", fh->set_tempo.tempo); + return; + } + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_TIME_SIGNATURE) { + printf("UMP Set Time Signature value %d / %d, num_notes %d\n", + fh->set_time_sig.numerator, fh->set_time_sig.denominator, + fh->set_time_sig.num_notes); + return; + } + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_METRONOME) { + printf("UMP Set Metronome clock %d, bar %d/%d/%d, sub %d/%d\n", + fh->set_metronome.clocks_primary, + fh->set_metronome.bar_accent_1, + fh->set_metronome.bar_accent_2, + fh->set_metronome.bar_accent_3, + fh->set_metronome.subdivision_1, + fh->set_metronome.subdivision_2); + return; + } + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_KEY_SIGNATURE) { + printf("UMP Set Key Signature sharps/flats %d, tonic %d\n", + fh->set_key_sig.sharps_flats, + fh->set_key_sig.tonic_note); + return; + } + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_CHORD_NAME) { + printf("UMP Set Chord Name tonic %d %d %d, alt1 %d/%d, alt2 %d/%d, alt3 %d/%d, alt4 %d/%d, bass %d %d %d, alt1 %d/%d alt2 %d/%d\n", + fh->set_chord_name.tonic_sharp, + fh->set_chord_name.chord_tonic, + fh->set_chord_name.chord_type, + fh->set_chord_name.alter1_type, + fh->set_chord_name.alter1_degree, + fh->set_chord_name.alter2_type, + fh->set_chord_name.alter2_degree, + fh->set_chord_name.alter3_type, + fh->set_chord_name.alter3_degree, + fh->set_chord_name.alter4_type, + fh->set_chord_name.alter4_degree, + fh->set_chord_name.bass_sharp, + fh->set_chord_name.bass_note, + fh->set_chord_name.bass_type, + fh->set_chord_name.bass_alter1_type, + fh->set_chord_name.bass_alter1_type, + fh->set_chord_name.bass_alter2_degree, + fh->set_chord_name.bass_alter2_degree); + return; + } + + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_METADATA || + fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_PERF_TEXT) { + printf("Meta (%s) ", ump_meta_prefix(fh)); + print_ump_string(ump, fh->meta.format, 4, 12); + printf("\n"); + return; + } + + printf("Flex Data: channel = %d, format = %d, addrs = %d, status_bank = %d, status = %d\n", + fh->meta.channel, fh->meta.format, fh->meta.addrs, + fh->meta.status_bank, fh->meta.status); +} + +static void dump_ump_event(const snd_seq_ump_event_t *ev) +{ + if (!snd_seq_ev_is_ump(ev)) { + dump_event((const snd_seq_event_t *)ev); + return; + } + + printf("%3d:%-3d ", ev->source.client, ev->source.port); + + switch (snd_ump_msg_type(ev->ump)) { + case SND_UMP_MSG_TYPE_UTILITY: + dump_ump_utility_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_SYSTEM: + dump_ump_system_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE: + dump_ump_midi1_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_MIDI2_CHANNEL_VOICE: + dump_ump_midi2_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_DATA: + dump_ump_sysex_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_EXTENDED_DATA: + dump_ump_extended_data_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_FLEX_DATA: + dump_ump_flex_data_event(ev->ump); + break; + case SND_UMP_MSG_TYPE_STREAM: + dump_ump_stream_event(ev->ump); + break; + default: + printf("UMP event: type = %d, group = %d, status = %d, 0x%08x\n", + snd_ump_msg_type(ev->ump), + snd_ump_msg_group(ev->ump), + snd_ump_msg_status(ev->ump), + *ev->ump); + break; + } +} + +static void list_ports(void) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + puts(" Port Client name Port name"); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + /* we need both READ and SUBS_READ */ + if ((snd_seq_port_info_get_capability(pinfo) + & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) + != (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) + continue; + printf("%3d:%-3d %-32.32s %s\n", + snd_seq_port_info_get_client(pinfo), + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } +} + +static void help(const char *argv0) +{ + printf("Usage: %s [options]\n" + "\nAvailable options:\n" + " -h,--help this help\n" + " -V,--version show version\n" + " -l,--list list input ports\n" + " -N,--normalized-view show normalized values\n" + " -P,--percent-view show percent values\n" + " -R,--raw-view show raw values (default)\n" + " -u,--ump=version set client MIDI version (0=legacy, 1= UMP MIDI 1.0, 2=UMP MIDI2.0)\n" + " -r,--raw do not convert UMP and legacy events\n" + " -p,--port=client:port,... source port(s)\n", + argv0); +} + +static void version(void) +{ + puts("aseqdump version " SND_UTIL_VERSION_STR); +} + +static void sighandler(int sig ATTRIBUTE_UNUSED) +{ + stop = 1; +} + +int main(int argc, char *argv[]) +{ + static const char short_options[] = "hVlp:NPRu:r"; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"list", 0, NULL, 'l'}, + {"port", 1, NULL, 'p'}, + {"normalized-view", 0, NULL, 'N'}, + {"percent-view", 0, NULL, 'P'}, + {"raw-view", 0, NULL, 'R'}, + {"ump", 1, NULL, 'u'}, + {"raw", 0, NULL, 'r'}, + {0} + }; + + int do_list = 0; + struct pollfd *pfds; + int npfds; + int c, err; + + init_seq(); + + while ((c = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (c) { + case 'h': + help(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'l': + do_list = 1; + break; + case 'p': + parse_ports(optarg); + break; + case 'R': + view_mode = VIEW_RAW; + break; + case 'P': + view_mode = VIEW_PERCENT; + break; + case 'N': + view_mode = VIEW_NORMALIZED; + break; + case 'u': + ump_version = atoi(optarg); + if (ump_version < 0 || ump_version > 2) + fatal("Invalid UMP version %d", ump_version); + snd_seq_set_client_midi_version(seq, ump_version); + break; + case 'r': + snd_seq_set_client_ump_conversion(seq, 0); + break; + default: + help(argv[0]); + return 1; + } + } + if (optind < argc) { + help(argv[0]); + return 1; + } + + if (do_list) { + list_ports(); + return 0; + } + + create_port(); + connect_ports(); + + err = snd_seq_nonblock(seq, 1); + check_snd("set nonblock mode", err); + + if (port_count > 0) + printf("Waiting for data."); + else + printf("Waiting for data at port %d:0.", + snd_seq_client_id(seq)); + printf(" Press Ctrl+C to end.\n"); + printf("Source %sEvent Ch Data\n", + ump_version ? "Group " : ""); + + signal(SIGINT, sighandler); + signal(SIGTERM, sighandler); + + npfds = snd_seq_poll_descriptors_count(seq, POLLIN); + pfds = alloca(sizeof(*pfds) * npfds); + for (;;) { + snd_seq_poll_descriptors(seq, pfds, npfds, POLLIN); + if (poll(pfds, npfds, -1) < 0) + break; + for (;;) { + snd_seq_event_t *event; + snd_seq_ump_event_t *ump_ev; + if (ump_version > 0) { + err = snd_seq_ump_event_input(seq, &ump_ev); + if (err < 0) + break; + if (ump_ev) + dump_ump_event(ump_ev); + continue; + } + + err = snd_seq_event_input(seq, &event); + if (err < 0) + break; + if (event) + dump_event(event); + } + fflush(stdout); + if (stop) + break; + } + + snd_seq_close(seq); + return 0; +} diff --git a/seq/aseqnet/Makefile.am b/seq/aseqnet/Makefile.am new file mode 100644 index 0000000..852d1e9 --- /dev/null +++ b/seq/aseqnet/Makefile.am @@ -0,0 +1,7 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = README.aseqnet aseqnet.1 + +bin_PROGRAMS = aseqnet +aseqnet_SOURCES = aseqnet.c +aseqnet_LDADD = $(INTLLIBS) +man_MANS = aseqnet.1 diff --git a/seq/aseqnet/README.aseqnet b/seq/aseqnet/README.aseqnet new file mode 100644 index 0000000..6a627f4 --- /dev/null +++ b/seq/aseqnet/README.aseqnet @@ -0,0 +1,55 @@ +================================================================ + ALSA sequencer connectors over network + ver.0.1 + Copyright (C) 1999-2000 Takashi Iwai +================================================================ + +* ASEQNET + +aseqnet is a sequencer client which sends/receives events over +network. Suppose two hosts (hostA and hostB) connected by network. +You need to run ALSA system on both hosts. Then, start aseqnet as a +server on hostA: + + hostA% aseqnet + sequencer opened: 128:0 + +A user client 128 with port 0 was opened. (The client number may +vary.) At next, start client on hostB. The argument is the hostname +where server is running. + + hostB% aseqnet hostA + sequencer opened: 132:0 + +Now events sent to hostA:128:0 is transferred to hostB:132:0, and vice +versa. + +You can connect these ports arbitrary to other sequencer ports. +For example, connect hostB:132:0 to a MIDI output device 65:0. The +aconnect utility can be used for this: + + hostB% aconnect 132:0 65:0 + +Events to hostA:128:0 will be delivered indirectly to hostB:65:0. +You'll hear MIDI sounds as following: + + hostA% pmidi -p 128:0 foo.mid + +The multiple clients may exist simultaneously. If hostC is connected +as a client to hostA, events from from hostA are sent to all connected +network clients, hostB and hostC. However, only one connection is +allowed from a client to a server. + +To disconnect network, stop all clients before server by ctrl-C or +sending signal to them. The server will automatically quit. + +The available options are: + + -p port : specify the TCP port number or TCP service name. + Default value is 40002. + -s addr : explicit read-subscription to the given address + (client:addr). + -d addr : explicit write-subscription to the given address. + -n name : specify the midi name of the process. + Default value is either 'Net Client' or 'Net Server'. + -v : verbose mode. diff --git a/seq/aseqnet/aseqnet.1 b/seq/aseqnet/aseqnet.1 new file mode 100644 index 0000000..6ed3911 --- /dev/null +++ b/seq/aseqnet/aseqnet.1 @@ -0,0 +1,83 @@ +.TH aseqnet 1 "January 1, 2000" +.SH NAME +aseqnet \- ALSA sequencer connectors over network + +.SH SYNOPSIS +.B aseqnet +[remotehost] + +.SH DESCRIPTION +.B aseqnet +is an ALSA sequencer client which sends and receives event packets +over network. +Suppose two hosts connected by network, +.I hostA +as a server +and +.I hostB +as a client. +The ALSA sequencer system must be running on both hosts. +For creating the server port, run the following on hostA: +.IP "" 4 +hostA% aseqnet +.br +sequencer opened: 128:0 +.PP +Then a user client 128 with port 0 was opened on hostA. +(The client number may vary.) +For creating the (network-)client port, run +.B aseqnet +with the hostname of the server: +.IP "" 4 +hostB% aseqnet hostA +.br +sequencer opened: 132:0 +.PP +Now all events sent to hostA:128:0 are transferred to hostB:132:0, and vice +versa. +.PP +The ports created by +.B aseqnet +can be connected arbitrary to other sequencer ports via +.B aconnect(1). +For example, to connect hostB:132:0 to a MIDI output device 65:0: +.IP "" 4 +hostB% aconnect 132:0 65:0 +.PP +Then events to hostA:128:0 will be delivered to hostB:65:0. +The following command plays MIDI on +.I hostB. +.IP "" 4 +hostA% pmidi \-p 128:0 foo.mid +.PP +The multiple clients may exist simultaneously. If +.I hostC +is connected as a client to hostA, events from from hostA are sent +to all connected network clients, i.e. hostB and hostC. +However, only one connection is allowed from a client to a server. +.PP +To disconnect network, stop all clients before server by ctrl-C or +sending signal to them. The server will automatically quit. + +.SH OPTIONS +.TP +.B \-p port +Specify the TCP port number or TCP service name. +.TP +.B \-s addr +Subscribe to the given address for read automatically. +.TP +.B \-d addr +Subscribe to the given address for write automatically. +.TP +.B \-n name +Specify the midi name of the process. +.TP +.B \-v +Verbose mode. + +.SH "SEE ALSO" +aconnect(1), pmidi(1) + +.SH AUTHOR +Takashi Iwai . diff --git a/seq/aseqnet/aseqnet.c b/seq/aseqnet/aseqnet.c new file mode 100644 index 0000000..aa70b50 --- /dev/null +++ b/seq/aseqnet/aseqnet.c @@ -0,0 +1,665 @@ +/* + * network server/client for ALSA sequencer + * ver.0.1 + * + * Copyright (C) 1999-2000 Takashi Iwai + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gettext.h" + +/* + * prototypes + */ +static void usage(void); +static void init_buf(void); +static void init_pollfds(void); +static void close_files(void); +static void init_seq(char *source, char *dest, char *name); +static void sigterm_exit(int sig); +static void init_server(const char *port); +static void init_client(const char *server, const char *port); +static void do_loop(void); +static int copy_local_to_remote(void); +static int copy_remote_to_local(int fd); + +/* + * default TCP port number + */ +#define DEFAULT_PORT "40002" + +/* + * local input buffer + */ +static char *readbuf; +static int max_rdlen; +static char *writebuf; +static int cur_wrlen, max_wrlen; + +#define MAX_BUF_EVENTS 200 +#define MAX_CONNECTION 10 + +static snd_seq_t *handle; +static struct pollfd *seqifds = NULL; +static struct pollfd *seqofds = NULL; +static struct pollfd *pollfds = NULL; +static int seqifds_count = 0; +static int seqofds_count = 0; +static int pollfds_count = 0; +static int sockfd, netfd[MAX_CONNECTION] = {[0 ... MAX_CONNECTION-1] = -1}; +static int max_connection; +static int cur_connected; +static int seq_port; + +static int server_mode; +static int ipv6 = 0; +static int verbose = 0; +static int info = 0; + + +/* + * main routine + */ + +static const struct option long_option[] = { + {"ipv6", 0, NULL, '6'}, + {"port", 1, NULL, 'p'}, + {"source", 1, NULL, 's'}, + {"dest", 1, NULL, 'd'}, + {"name", 1, NULL, 'n'}, + {"help", 0, NULL, 'h'}, + {"verbose", 0, NULL, 'v'}, + {"info", 0, NULL, 'i'}, + {NULL, 0, NULL, 0}, +}; + +int main(int argc, char **argv) +{ + int c; + char *port = DEFAULT_PORT; + char *source = NULL, *dest = NULL; + char *name = NULL; + +#ifdef ENABLE_NLS + setlocale(LC_ALL, ""); + textdomain(PACKAGE); +#endif + + while ((c = getopt_long(argc, argv, "p:s:d:n:6hvi", long_option, NULL)) != -1) { + switch (c) { + case '6': + ipv6 = 1; + break; + case 'p': + port = optarg; + break; + case 's': + source = optarg; + break; + case 'd': + dest = optarg; + break; + case 'n': + name = optarg; + break; + case 'v': + verbose++; + break; + case 'i': + info++; + break; + default: + usage(); + exit(1); + } + } + + signal(SIGINT, sigterm_exit); + signal(SIGTERM, sigterm_exit); + + init_buf(); + init_seq(source, dest, name); + + if (optind >= argc) { + server_mode = 1; + max_connection = MAX_CONNECTION; + init_pollfds(); + init_server(port); + } else { + server_mode = 0; + max_connection = 1; + init_pollfds(); + init_client(argv[optind], port); + } + + do_loop(); + + close_files(); + + return 0; +} + + +/* + * print usage + */ +static void usage(void) +{ + printf(_("aseqnet - network client/server on ALSA sequencer\n")); + printf(_(" Copyright (C) 1999 Takashi Iwai\n")); + printf(_("usage:\n")); + printf(_(" server mode: aseqnet [-options]\n")); + printf(_(" client mode: aseqnet [-options] server_host\n")); + printf(_("options:\n")); + printf(_(" -6,--ipv6 : use IPv6 TCP protocol\n")); + printf(_(" -p,--port # : specify TCP port (digit or service name)\n")); + printf(_(" -s,--source addr : read from given addr (client:port)\n")); + printf(_(" -d,--dest addr : write to given addr (client:port)\n")); + printf(_(" -n,--name value : use a specific midi process name\n")); + printf(_(" -v, --verbose : print verbose messages\n")); + printf(_(" -i, --info : print certain received events\n")); +} + + +/* + * allocate and initialize buffers + */ +static void init_buf(void) +{ + max_wrlen = MAX_BUF_EVENTS * sizeof(snd_seq_event_t); + max_rdlen = MAX_BUF_EVENTS * sizeof(snd_seq_event_t); + writebuf = malloc(max_wrlen); + readbuf = malloc(max_rdlen); + if (writebuf == NULL || readbuf == NULL) { + fprintf(stderr, _("can't malloc\n")); + exit(1); + } + memset(writebuf, 0, max_wrlen); + memset(readbuf, 0, max_rdlen); + cur_wrlen = 0; +} + +/* + * allocate and initialize poll array + */ +static void init_pollfds(void) +{ + pollfds_count = seqifds_count + seqofds_count + 1 + max_connection; + pollfds = (struct pollfd *)calloc(pollfds_count, sizeof(struct pollfd)); + assert(pollfds); +} + +/* + * close all files + */ +static void close_files(void) +{ + int i; + if (verbose) + fprintf(stderr, _("closing files..\n")); + for (i = 0; i < max_connection; i++) { + if (netfd[i] >= 0) + close(netfd[i]); + } + if (sockfd >= 0) + close(sockfd); +} + + +/* + * initialize sequencer + */ +static void init_seq(char *source, char *dest, char* name) +{ + snd_seq_addr_t addr; + int err, counti, counto; + + if (snd_seq_open(&handle, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) { + perror("snd_seq_open"); + exit(1); + } + if (seqifds) + free(seqifds); + if (seqofds) + free(seqofds); + counti = seqifds_count = snd_seq_poll_descriptors_count(handle, POLLIN); + assert(counti > 0); + counto = seqofds_count = snd_seq_poll_descriptors_count(handle, POLLOUT); + assert(counto > 0); + seqifds = (struct pollfd *)calloc(counti, sizeof(struct pollfd)); + assert(seqifds); + seqofds = (struct pollfd *)calloc(counto, sizeof(struct pollfd)); + assert(seqofds); + err = snd_seq_poll_descriptors(handle, seqifds, counti, POLLIN); + assert(err == counti); + err = snd_seq_poll_descriptors(handle, seqofds, counto, POLLOUT); + assert(err == counto); + + snd_seq_nonblock(handle, 1); + + /* set client info */ + if (name) + snd_seq_set_client_name(handle, name); + else { + if (server_mode) + snd_seq_set_client_name(handle, "Net Server"); + else + snd_seq_set_client_name(handle, "Net Client"); + } + + /* create a port */ + seq_port = snd_seq_create_simple_port(handle, "Network", + SND_SEQ_PORT_CAP_READ | + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_READ | + SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC); + if (seq_port < 0) { + perror("create seq port"); + exit(1); + } + if (verbose) + fprintf(stderr, _("sequencer opened: %d:%d\n"), + snd_seq_client_id(handle), seq_port); + + /* explicit subscriptions */ + if (source) { + /* read subscription */ + if (snd_seq_parse_address(handle, &addr, source) < 0) { + fprintf(stderr, _("invalid source address %s\n"), source); + exit(1); + } + if (snd_seq_connect_from(handle, seq_port, addr.client, addr.port)) { + perror("read subscription"); + exit(1); + } + } + if (dest) { + /* write subscription */ + if (snd_seq_parse_address(handle, &addr, dest) < 0) { + fprintf(stderr, _("invalid destination address %s\n"), dest); + exit(1); + } + if (snd_seq_connect_to(handle, seq_port, addr.client, addr.port)) { + perror("write subscription"); + exit(1); + } + } +} + +/* + * translate the binary network address to ASCII + */ +static void get_net_addr(struct addrinfo *rp, char *buf, size_t buflen) +{ + void *ptr; + + switch (rp->ai_family) { + case AF_INET: + ptr = &((struct sockaddr_in *) rp->ai_addr)->sin_addr; + break; + case AF_INET6: + ptr = &((struct sockaddr_in6 *) rp->ai_addr)->sin6_addr; + break; + default: + ptr = NULL; + } + buf[buflen-1] = '\0'; + inet_ntop(rp->ai_family, ptr, buf, buflen-1); +} + +/* + * signal handler + */ +static void sigterm_exit(int sig ATTRIBUTE_UNUSED) +{ + close_files(); + exit(1); +} + + +/* + * initialize network server + */ +static void init_server(const char *port) +{ + struct addrinfo hints; + struct addrinfo *result, *rp; + char buf[100]; + int i; + int curstate = 1; + int save_errno = 0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = ipv6 ? AF_INET6 : AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + if (getaddrinfo(NULL, port, &hints, &result) < 0) { + fprintf(stderr, _("can't get address\n")); + exit(1); + } + for (rp = result; rp != NULL; rp = rp->ai_next) { + if ((sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) < 0){ + perror("create socket"); + exit(1); + } + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &curstate, sizeof(curstate)) < 0) { + perror("setsockopt"); + exit(1); + } + if (verbose) { + get_net_addr(rp, buf, sizeof(buf)); + fprintf(stderr, _("connecting to: %s\n"), buf); + } + if (bind(sockfd, rp->ai_addr, rp->ai_addrlen) == 0) + break; + save_errno = errno; + close(sockfd); + } + if (rp == NULL) { + errno = save_errno; + perror("bind"); + exit(1); + } + freeaddrinfo(result); + + if (listen(sockfd, 5) < 0) { + perror("can't listen"); + exit(1); + } + + cur_connected = 0; + for (i = 0; i < max_connection; i++) + netfd[i] = -1; +} + +/* + * start connection on server + */ +static void start_connection(void) +{ + struct sockaddr_in addr; + int i; + socklen_t addr_len; + + for (i = 0; i < max_connection; i++) { + if (netfd[i] < 0) + break; + } + if (i >= max_connection) { + fprintf(stderr, _("too many connections!\n")); + exit(1); + } + memset(&addr, 0, sizeof(addr)); + addr_len = sizeof(addr); + netfd[i] = accept(sockfd, (struct sockaddr *)&addr, &addr_len); + if (netfd[i] < 0) { + perror("accept"); + exit(1); + } + if (verbose) + fprintf(stderr, _("accepted[%d]\n"), netfd[i]); + cur_connected++; +} + +/* + * initialize network client + */ +static void init_client(const char *server, const char *port) +{ + struct addrinfo hints; + struct addrinfo *result, *rp; + char buf[100]; + int curstate = 1; + int fd; + int save_errno = 0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + if (getaddrinfo(server, port, &hints, &result) < 0) { + fprintf(stderr, _("can't get address %s\n"), server); + exit(1); + } + for (rp = result; rp != NULL; rp = rp->ai_next) { + if ((fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) < 0){ + perror("create socket"); + exit(1); + } + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &curstate, sizeof(curstate)) < 0) { + perror("setsockopt"); + exit(1); + } + if (verbose) { + get_net_addr(rp, buf, sizeof(buf)); + fprintf(stderr, _("connecting to: %s\n"), buf); + } + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) + break; + save_errno = errno; + close(fd); + } + if (rp == NULL) { + errno = save_errno; + perror("connect"); + exit(1); + } + freeaddrinfo(result); + if (verbose) + fprintf(stderr, _("ok.. connected\n")); + netfd[0] = fd; + cur_connected = 1; +} + +/* + * event loop + */ +static void do_loop(void) +{ + int i, rc, width; + int seqifd_ptr, sockfd_ptr = -1, netfd_ptr; + + for (;;) { + memset(pollfds, 0, pollfds_count * sizeof(struct pollfd)); + seqifd_ptr = 0; + memcpy(pollfds, seqifds, sizeof(*seqifds)*(width = seqifds_count)); + if (server_mode) { + sockfd_ptr = width; + pollfds[width].fd = sockfd; + pollfds[width].events = POLLIN; + width++; + } + netfd_ptr = width; + for (i = 0; i < max_connection; i++) { + if (netfd[i] >= 0) { + pollfds[width].fd = netfd[i]; + pollfds[width].events = POLLIN; + width++; + } + } + do { + rc = poll(pollfds, width, -1); + } while (rc <= 0 && errno == EINTR); + if (rc <= 0) { + perror("poll"); + exit(1); + } + if (server_mode) { + if (pollfds[sockfd_ptr].revents & (POLLIN|POLLOUT)) + start_connection(); + } + for (i = 0; i < seqifds_count; i++) + if (pollfds[seqifd_ptr + i].revents & (POLLIN|POLLOUT)) { + if (copy_local_to_remote()) + return; + break; + } + for (i = 0; i < max_connection; i++) { + if (netfd[i] < 0) + continue; + if (pollfds[netfd_ptr + i].revents & (POLLIN|POLLOUT)) { + if (copy_remote_to_local(netfd[i])) { + netfd[i] = -1; + cur_connected--; + if (cur_connected <= 0) + return; + } + } + } + } +} + + +/* + * flush write buffer - send data to the socket + */ +static void flush_writebuf(void) +{ + if (cur_wrlen) { + int i; + for (i = 0; i < max_connection; i++) { + if (netfd[i] >= 0) { + ssize_t wrlen = write(netfd[i], writebuf, cur_wrlen); + if (wrlen != (ssize_t)cur_wrlen) + fprintf(stderr, "write error: %s", wrlen < 0 ? strerror(errno) : "short"); + } + } + cur_wrlen = 0; + } +} + +/* + * get space from write buffer + */ +static char *get_writebuf(int len) +{ + char *buf; + if (cur_wrlen + len >= max_wrlen) + flush_writebuf(); + buf = writebuf + cur_wrlen; + cur_wrlen += len; + return buf; +} + +static void print_event(snd_seq_event_t *ev) +{ + switch (ev->type) { + case SND_SEQ_EVENT_CONTROLLER: + printf(_("Channel %2d: Control event : %5d\n"), + ev->data.control.channel, ev->data.control.value); + break; + case SND_SEQ_EVENT_PITCHBEND: + printf(_("Channel %2d: Pitchbender : %5d\n"), + ev->data.control.channel, ev->data.control.value); + break; + case SND_SEQ_EVENT_NOTEON: + printf(_("Channel %2d: Note On event : %5d\n"), + ev->data.control.channel, ev->data.note.note); + break; + case SND_SEQ_EVENT_NOTEOFF: + printf(_("Channel %2d: Note Off event: %5d\n"), + ev->data.control.channel, ev->data.note.note); + break; + } +} + +#define EVENT_PACKET_SIZE 32 + +/* + * copy events from sequencer to port(s) + */ +static int copy_local_to_remote(void) +{ + int rc; + snd_seq_event_t *ev; + char *buf; + + while ((rc = snd_seq_event_input(handle, &ev)) >= 0 && ev) { + if (ev->type >= SND_SEQ_EVENT_CLIENT_START && + ! snd_seq_ev_is_variable_type(ev)) { + snd_seq_free_event(ev); + continue; + } + if (snd_seq_ev_is_variable(ev)) { + int len; + len = EVENT_PACKET_SIZE + ev->data.ext.len; + buf = get_writebuf(len); + memcpy(buf, ev, sizeof(snd_seq_event_t)); + memcpy(buf + EVENT_PACKET_SIZE, ev->data.ext.ptr, ev->data.ext.len); + } else { + buf = get_writebuf(EVENT_PACKET_SIZE); + memcpy(buf, ev, EVENT_PACKET_SIZE); + } + if (info) + print_event(ev); + snd_seq_free_event(ev); + } + flush_writebuf(); + return 0; +} + +/* + * copy events from a port to sequencer + */ +static int copy_remote_to_local(int fd) +{ + int count; + char *buf; + snd_seq_event_t *ev; + + count = read(fd, readbuf, MAX_BUF_EVENTS * sizeof(snd_seq_event_t)); + buf = readbuf; + + if (count == 0) { + if (verbose) + fprintf(stderr, _("disconnected\n")); + return 1; + } + + while (count > 0) { + ev = (snd_seq_event_t*)buf; + buf += EVENT_PACKET_SIZE; + count -= EVENT_PACKET_SIZE; + if (snd_seq_ev_is_variable(ev) && ev->data.ext.len > 0) { + ev->data.ext.ptr = buf; + buf += ev->data.ext.len; + count -= ev->data.ext.len; + } + snd_seq_ev_set_direct(ev); + snd_seq_ev_set_source(ev, seq_port); + snd_seq_ev_set_subs(ev); + if (info) + print_event(ev); + snd_seq_event_output(handle, ev); + } + + snd_seq_drain_output(handle); + return 0; +} + diff --git a/seq/aseqsend/Makefile.am b/seq/aseqsend/Makefile.am new file mode 100644 index 0000000..62da1ef --- /dev/null +++ b/seq/aseqsend/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = aseqsend.1 + +bin_PROGRAMS = aseqsend +man_MANS = aseqsend.1 diff --git a/seq/aseqsend/aseqsend.1 b/seq/aseqsend/aseqsend.1 new file mode 100644 index 0000000..2fd8a02 --- /dev/null +++ b/seq/aseqsend/aseqsend.1 @@ -0,0 +1,77 @@ +.TH ASEQSEND 1 "11 Mar 2024" + +.SH NAME +aseqsend \- send arbitrary messages to selected ALSA MIDI seqencer port + +.SH SYNOPSIS +\fBaseqsend\fP \-p client:port -s file-name +.br +\fBaseqsend\fP \-p client:port "hex encoded byte-string" + +.SH DESCRIPTION +\fBaseqsend\fP is a command-line utility which allows one to send +SysEx (system exclusive) data to ALSA MIDI sequencer port. +It can also send any other MIDI commands. +Messages to be sent can be given in the last argument as hex encoded +byte string or can be read from raw binary file. +When sending several SysEx messages at once there is a delay of 1ms +after each message as default and can be set to different value with +option \-i. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +\fBaseqsend\fP can send UMP packets as MIDI 2.0 device by specifying +via \-u option as well, while the default operation is the legacy MIDI +1.0 byte stream. + +.SH OPTIONS + +.TP +\fI\-h, \-\-help\fP +Prints a list of options. + +.TP +\fI\-V, \-\-version\fP +Prints the current version. + +.TP +\fI\-l, \-\-list\FP +Prints a list of possible output ports. + +.TP +\fI\-v, \-\-verbose\fP +Prints number of bytes actually sent + +.TP +\fI\-p, -\-port=client:port\fP +Target port by number or name + +.TP +\fI\-s, \-\-file=filename\fP +Send raw binary data from given file name + +.TP +\fI\-i, \-\-interval=msec\fP +Interval between SysEx messages in milliseconds + +.TP +\fI\-u, \-\-ump=version\fP +Specify the MIDI version. 0 for the legacy MIDI 1.0 (default), +1 for UMP MIDI 1.0 protocol and 2 for UMP MIDI 2.0 protocol. + +When UMP MIDI 1.0 or MIDI 2.0 protocol is specified, \fBaseqsend\fP +reads the input as raw UMP packets, 4 each byte in big endian. + +.SH EXAMPLES + +\fBaseqsend -p 128:0 "F0 41 10 00 00 64 12 18 00 21 06 59 41 59 4E F7"\fP + +\fBaseqsend -p 128:0 -s I7BulkDump.syx\fP + +.SH SEE ALSO +\fBaseqdump(1)\fP + +.SH AUTHOR +Miroslav Kovac diff --git a/seq/aseqsend/aseqsend.c b/seq/aseqsend/aseqsend.c new file mode 100644 index 0000000..92354eb --- /dev/null +++ b/seq/aseqsend/aseqsend.c @@ -0,0 +1,458 @@ +/* + * aseqsend.c - send arbitrary MIDI messages to selected ALSA MIDI seqencer port + * + * Copyright (c) 2005 Clemens Ladisch + * Copyright (c) 2024 Miroslav Kovac + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define _GNU_SOURCE +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef unsigned char mbyte_t; + +static snd_seq_t *seq; +static char *port_name = NULL; +static char *send_file_name = NULL; +static char *send_hex; +static mbyte_t *send_data; +static snd_seq_addr_t addr; +static int send_data_length; +static int sent_data_c; +static int ump_version; +static int sysex_interval = 1000; //us +static snd_midi_event_t *edev; + +static void error(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vfprintf(stderr, format, ap); + va_end(ap); + putc('\n', stderr); +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +static void usage(void) +{ + printf( + "\nUsage: aseqsend -p target-port -s file-name|\"hex encoded bytes\"\n\n" + " -h,--help this help\n" + " -V,--version print current version\n" + " -v,--verbose verbose mode\n" + " -l,--list list all sequencer ports\n" + " -p,--port=c:p target port by number or name\n" + " -s,--file=name send binary data from given file name\n" + " -i,--interval=v interval between SysEx messages in miliseconds\n" + " -u,--ump=version MIDI version: 0=legacy (default), 1=MIDI1, 2=MIDI2\n\n"); +} + +static void version(void) +{ + puts("aseqsend version " SND_UTIL_VERSION_STR); +} + +static void *my_malloc(size_t size) +{ + void *p = malloc(size); + if (!p) { + fatal("out of memory"); + exit(EXIT_FAILURE); + } + return p; +} + +static int hex_value(char c) +{ + if ('0' <= c && c <= '9') + return c - '0'; + if ('A' <= c && c <= 'F') + return c - 'A' + 10; + if ('a' <= c && c <= 'f') + return c - 'a' + 10; + error("invalid character %c", c); + return -1; +} + +static void parse_data(void) +{ + const char *p; + int i, value; + + send_data = my_malloc(strlen(send_hex)); + i = 0; + value = -1; /* value is >= 0 when the first hex digit of a byte has been read */ + for (p = send_hex; *p; ++p) { + int digit; + if (isspace((unsigned char)*p)) { + if (value >= 0) { + send_data[i++] = value; + value = -1; + } + continue; + } + digit = hex_value(*p); + if (digit < 0) { + exit(EXIT_FAILURE); + } + if (value < 0) { + value = digit; + } else { + send_data[i++] = (value << 4) | digit; + value = -1; + } + } + if (value >= 0) + send_data[i++] = value; + send_data_length = i; +} + +static void add_send_hex_data(const char *str) +{ + int length; + char *s; + + length = (send_hex ? strlen(send_hex) + 1 : 0) + strlen(str) + 1; + s = my_malloc(length); + if (send_hex) { + strcpy(s, send_hex); + strcat(s, " "); + } else { + s[0] = '\0'; + } + strcat(s, str); + free(send_hex); + send_hex = s; +} + +static void load_file(void) +{ + int fd; + off_t length; + + fd = open(send_file_name, O_RDONLY); + if (fd == -1) { + error("cannot open %s - %s", send_file_name, strerror(errno)); + return; + } + length = lseek(fd, 0, SEEK_END); + if (length == (off_t)-1) { + error("cannot determine length of %s: %s", send_file_name, strerror(errno)); + goto _error; + } + send_data = my_malloc(length); + lseek(fd, 0, SEEK_SET); + if (read(fd, send_data, length) != length) { + error("cannot read from %s: %s", send_file_name, strerror(errno)); + goto _error; + } + if (length >= 4 && !memcmp(send_data, "MThd", 4)) { + error("%s is a Standard MIDI File; use aplaymidi to send it", send_file_name); + goto _error; + } + send_data_length = length; + goto _exit; +_error: + free(send_data); + send_data = NULL; +_exit: + close(fd); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_OUTPUT, 0); + check_snd("open sequencer", err); + + /* set our client's name */ + err = snd_seq_set_client_name(seq, "aseqsend"); + check_snd("set client name", err); + + err = snd_seq_set_client_midi_version(seq, ump_version); + check_snd("set client midi version", err); +} + +static void create_port(void) +{ + int err; + + err = snd_seq_create_simple_port(seq, "aseqsend", + SND_SEQ_PORT_CAP_READ, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + check_snd("create port", err); +} + +static void init_midi_event_encoder(void) +{ + int err; + + err = snd_midi_event_new(256, &edev); + check_snd("create midi event encoder", err); +} + +static void list_ports(void) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + puts(" Port Client name Port name"); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + + if ((snd_seq_port_info_get_capability(pinfo) + & SND_SEQ_PORT_CAP_WRITE) + != SND_SEQ_PORT_CAP_WRITE) + continue; + printf("%3d:%-3d %-32.32s %s\n", + snd_seq_port_info_get_client(pinfo), + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } +} + +/* compose a UMP event, submit it, return the next data position */ +static int send_ump(int pos) +{ + int ump_len = 0, offset = 0; + unsigned int ump[4]; + snd_seq_ump_event_t ev; + + snd_seq_ump_ev_clear(&ev); + snd_seq_ev_set_source(&ev, 0); + snd_seq_ev_set_dest(&ev, addr.client, addr.port); + snd_seq_ev_set_direct(&ev); + + do { + const mbyte_t *data = send_data + pos; + + if (pos >= send_data_length) + return pos; + ump[offset] = (data[0] << 24) | (data[1] << 16) | + (data[2] << 8) | data[3]; + if (!offset) + ump_len = snd_ump_packet_length(snd_ump_msg_type(ump)); + offset++; + pos += 4; + } while (offset < ump_len); + + snd_seq_ev_set_ump_data(&ev, ump, ump_len * 4); + snd_seq_ump_event_output(seq, &ev); + snd_seq_drain_output(seq); + + sent_data_c += ump_len * 4; + return pos; +} + +/* compose an event, submit it, return the next data position */ +static int send_midi_bytes(int pos) +{ + const mbyte_t *data = send_data + pos; + snd_seq_event_t ev; + int is_sysex = 0; + int end; + + snd_seq_ev_clear(&ev); + snd_seq_ev_set_source(&ev, 0); + snd_seq_ev_set_dest(&ev, addr.client, addr.port); + snd_seq_ev_set_direct(&ev); + + if (send_data[pos] == 0xf0) { + is_sysex = 1; + for (end = pos + 1; end < send_data_length; end++) { + if (send_data[end] == 0xf7) + break; + } + + if (end == send_data_length) + fatal("SysEx is missing terminating byte (0xF7)"); + end++; + snd_seq_ev_set_sysex(&ev, end - pos, send_data + pos); + } else { + end = pos; + while (!snd_midi_event_encode_byte(edev, *data++, &ev)) { + if (++end >= send_data_length) + return end; + } + + end++; + } + + snd_seq_event_output(seq, &ev); + snd_seq_drain_output(seq); + if (is_sysex) + usleep(sysex_interval); + + sent_data_c += end - pos; + return end; +} + +int main(int argc, char *argv[]) +{ + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"verbose", 0, NULL, 'v'}, + {"list", 0, NULL, 'l'}, + {"port", 1, NULL, 'p'}, + {"file", 1, NULL, 's'}, + {"interval", 1, NULL, 'i'}, + {"ump", 1, NULL, 'u'}, + {0} + }; + int c = 0; + char do_send_file = 0; + char do_port_list = 0; + char verbose = 0; + int k; + + while ((c = getopt_long(argc, argv, "hi:Vvlp:s:u:", long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage(); + return 0; + case 'V': + version(); + return 0; + case 'v': + verbose = 1; + break; + case 'l': + do_port_list = 1; + break; + case 'p': + port_name = optarg; + break; + case 's': + send_file_name = optarg; + do_send_file = 1; + break; + case 'i': + sysex_interval = atoi(optarg) * 1000; //ms--->us + break; + case 'u': + ump_version = atoi(optarg); + break; + default: + error("Try 'aseqsend -h' for more information."); + exit(EXIT_FAILURE); + } + } + + if (argc < 2) { + usage(); + exit(EXIT_FAILURE); + } + + if (do_port_list){ + init_seq(); + list_ports(); + exit(EXIT_SUCCESS); + } + + if (port_name == NULL) + fatal("Output port must be specified!"); + + if (do_send_file) { + load_file(); + } else { + /* no file specified ---> send hex bytes from cmd arguments*/ + /* data for send can be specified as multiple arguments */ + for (; argv[optind]; ++optind) { + add_send_hex_data(argv[optind]); + } + if (send_hex) + parse_data(); + } + + if (!send_data) + exit(EXIT_SUCCESS); + + if (ump_version && (send_data_length % 4) != 0) + fatal("UMP data must be aligned to 4 bytes"); + + init_seq(); + create_port(); + if (!ump_version) + init_midi_event_encoder(); + + if (snd_seq_parse_address(seq, &addr, port_name) < 0) { + error("Unable to parse port name!"); + exit(EXIT_FAILURE); + } + + sent_data_c = 0; //counter of actually sent bytes + + k = 0; + while (k < send_data_length) { + if (ump_version) + k = send_ump(k); + else + k = send_midi_bytes(k); + } + + if (verbose) + printf("Sent : %u bytes\n", sent_data_c); + + snd_seq_close(seq); + exit(EXIT_SUCCESS); +} diff --git a/speaker-test/Makefile.am b/speaker-test/Makefile.am new file mode 100644 index 0000000..b21919b --- /dev/null +++ b/speaker-test/Makefile.am @@ -0,0 +1,9 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +SUBDIRS= samples +LDADD = $(LIBINTL) -lm + +bin_PROGRAMS = speaker-test +speaker_test_SOURCES = speaker-test.c pink.c st2095.c +man_MANS = speaker-test.1 +EXTRA_DIST = readme.txt speaker-test.1 pink.h st2095.h + diff --git a/speaker-test/pink.c b/speaker-test/pink.c new file mode 100644 index 0000000..63fe296 --- /dev/null +++ b/speaker-test/pink.c @@ -0,0 +1,97 @@ +/* + patest_pink.c + + generate Pink Noise using Gardner method. + Optimization suggested by James McCartney uses a tree + to select which random value to replace. + + x x x x x x x x x x x x x x x x + x x x x x x x x + x x x x + x x + x + + Tree is generated by counting trailing zeros in an increasing index. + When the index is zero, no random number is selected. + + This program uses the Portable Audio library which is under development. + For more information see: http://www.audiomulch.com/portaudio/ + + Author: Phil Burk, http://www.softsynth.com + + Revision History: + + Copyleft 1999 Phil Burk - No rights reserved. +*/ + +#include "aconfig.h" +#include +#include +#include "pink.h" + +/************************************************************/ +/* Calculate pseudo-random 32 bit number based on linear congruential method. */ +static unsigned long generate_random_number( void ) +{ + static unsigned long rand_seed = 22222; /* Change this for different random sequences. */ + rand_seed = (rand_seed * 196314165) + 907633515; + return rand_seed; +} + +/* Setup PinkNoise structure for N rows of generators. */ +void initialize_pink_noise( pink_noise_t *pink, int num_rows ) +{ + int i; + long pmax; + pink->pink_index = 0; + pink->pink_index_mask = (1<pink_scalar = 1.0f / pmax; +/* Initialize rows. */ + for( i=0; ipink_rows[i] = 0; + pink->pink_running_sum = 0; +} + +/* generate Pink noise values between -1.0 and +1.0 */ +float generate_pink_noise_sample( pink_noise_t *pink ) +{ + long new_random; + long sum; + float output; + +/* Increment and mask index. */ + pink->pink_index = (pink->pink_index + 1) & pink->pink_index_mask; + +/* If index is zero, don't update any random values. */ + if( pink->pink_index != 0 ) + { + /* Determine how many trailing zeros in PinkIndex. */ + /* This algorithm will hang if n==0 so test first. */ + int num_zeros = 0; + int n = pink->pink_index; + while( (n & 1) == 0 ) + { + n = n >> 1; + num_zeros++; + } + + /* Replace the indexed ROWS random value. + * Subtract and add back to Running_sum instead of adding all the random + * values together. Only one changes each time. + */ + pink->pink_running_sum -= pink->pink_rows[num_zeros]; + new_random = ((long)generate_random_number()) >> PINK_RANDOM_SHIFT; + pink->pink_running_sum += new_random; + pink->pink_rows[num_zeros] = new_random; + } + +/* Add extra white noise value. */ + new_random = ((long)generate_random_number()) >> PINK_RANDOM_SHIFT; + sum = pink->pink_running_sum + new_random; + +/* Scale to range of -1.0 to 0.9999. */ + output = pink->pink_scalar * sum; + + return output; +} diff --git a/speaker-test/pink.h b/speaker-test/pink.h new file mode 100644 index 0000000..0d17f7a --- /dev/null +++ b/speaker-test/pink.h @@ -0,0 +1,15 @@ +#define PINK_MAX_RANDOM_ROWS (30) +#define PINK_RANDOM_BITS (24) +#define PINK_RANDOM_SHIFT ((sizeof(long)*8)-PINK_RANDOM_BITS) + +typedef struct +{ + long pink_rows[PINK_MAX_RANDOM_ROWS]; + long pink_running_sum; /* Used to optimize summing of generators. */ + int pink_index; /* Incremented each sample. */ + int pink_index_mask; /* Index wrapped by ANDing with this mask. */ + float pink_scalar; /* Used to scale within range of -1.0 to +1.0 */ +} pink_noise_t; + +void initialize_pink_noise( pink_noise_t *pink, int num_rows ); +float generate_pink_noise_sample( pink_noise_t *pink ); diff --git a/speaker-test/readme.txt b/speaker-test/readme.txt new file mode 100644 index 0000000..6774fe0 --- /dev/null +++ b/speaker-test/readme.txt @@ -0,0 +1,12 @@ +To make or build just type + +make + +To test: - +1) Just stereo sound from one stereo jack: - +./speaker-test -Dplug:front -c2 +2) A 4 speaker setup from two stereo jacks: - +./speaker-test -Dplug:surround40 -c4 +3) A 5.1 speaker setup from three stereo jacks: - +./speaker-test -Dplug:surround51 -c6 + diff --git a/speaker-test/samples/Front_Center.wav b/speaker-test/samples/Front_Center.wav new file mode 100644 index 0000000000000000000000000000000000000000..4f121805621c1e274af95e1a9307dc5dff411e7b GIT binary patch literal 137134 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%0A{U;GXU3=BnpP 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 literal 0 HcmV?d00001 diff --git a/speaker-test/samples/Front_Left.wav b/speaker-test/samples/Front_Left.wav new file mode 100644 index 0000000000000000000000000000000000000000..9d6ebdd85f235e12934ee9a0069258601da78ffc GIT binary patch literal 142128 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 literal 0 HcmV?d00001 diff --git a/speaker-test/samples/Front_Right.wav b/speaker-test/samples/Front_Right.wav new file mode 100644 index 0000000000000000000000000000000000000000..441565c8ee7efb552e7bb9e01afd2e5443c23f3f GIT binary patch literal 146990 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

aLniek$aEQqU83-U;T*&7ixy*53w3=?7F$ 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

PV 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

S=u7^=ANY{oyKSA(_9Odu$lh5jKY~(~>3(kO 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 literal 0 HcmV?d00001 diff --git a/speaker-test/samples/Rear_Center.wav b/speaker-test/samples/Rear_Center.wav new file mode 100644 index 0000000000000000000000000000000000000000..ee2c6da904594e9af29a0ab7825a5d1475ef62c7 GIT binary patch literal 130096 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 z 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 E2R9U-SO5S3 literal 0 HcmV?d00001 diff --git a/speaker-test/samples/Rear_Left.wav b/speaker-test/samples/Rear_Left.wav new file mode 100644 index 0000000000000000000000000000000000000000..57d031158c00175ce535b310554f9eee95c9b48a GIT binary patch literal 126064 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�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 Sk*t4sej$Ez>BrdGHtT-_SNr7v literal 0 HcmV?d00001 diff --git a/speaker-test/samples/Rear_Right.wav b/speaker-test/samples/Rear_Right.wav new file mode 100644 index 0000000000000000000000000000000000000000..692e646bed38b0de657c20a95786f73b5e9bb07b GIT binary patch literal 146480 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 literal 0 HcmV?d00001 diff --git a/speaker-test/samples/Side_Right.wav b/speaker-test/samples/Side_Right.wav new file mode 100644 index 0000000000000000000000000000000000000000..b0e5b3a88bd44938c81238dcb4243e89c2f52c7d GIT binary patch literal 129966 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 z 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

;3L6RI1iuRg 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 B1dRXy literal 0 HcmV?d00001 diff --git a/speaker-test/speaker-test.1 b/speaker-test/speaker-test.1 new file mode 100644 index 0000000..f375e19 --- /dev/null +++ b/speaker-test/speaker-test.1 @@ -0,0 +1,189 @@ +.\" -*- nroff -*- +.\" macros +.de EX \" Begin Example +. IP +. ft CW +. nf +. ne \\$1 +.. +.de EE \" End Example +. ft P +. fi +. PP +.. +.TH SPEAKER\-TEST 1 "April 2nd, 2011" speaker\-test +.SH NAME +speaker\-test \- command\-line speaker test tone generator for ALSA +.SH SYNOPSIS +.B speaker\-test [\-options] + +.SH DESCRIPTION +\fBspeaker\-test\fP generates a tone that can be used to test the speakers of a computer. + +\fBspeaker\-test\fP by default will test the \fIdefault\fP device. If you +want to test another sound device you will have first to get a list of +all of the sound cards in your system and the devices associated with +those cards. Notice that there might be for example, one device for +analog sound, one for digital sound and one for HDMI sound. +To get the list of available cards and devices you can run \fBaplay \-L\fR. + +.P \fBaplay\fR's output will be similar to this one: + +.nf +$ aplay \-L +null + Discard all samples (playback) or generate zero samples (capture) +default:CARD=ICH5 + Intel ICH5, Intel ICH5 + Default Audio Device +front:CARD=ICH5,DEV=0 + Intel ICH5, Intel ICH5 + Front speakers +surround40:CARD=ICH5,DEV=0 + Intel ICH5, Intel ICH5 + 4.0 Surround output to Front and Rear speakers +(...) +.fi + +.P Each of the devices is listed in the beginning of the definition so, +in the above example, there are four devices listed: null, default, front +and surround40. So, if you want to test the last device you can +run \fBspeaker\-test \-Dsurround40:ICH5 \-c 6\fR. The \fB\-c\fR option will +indicate that the six audio channels in the device have to be tested. + + + + + +.SH OPTIONS + +.TP +\fB\-c\fP | \fB\-\-channels\fP \fINUM\fP +\fINUM\fP channels in stream + +.TP +\fB\-D\fP | \fB\-\-device\fP \fINAME\fP +PCM device name \fINAME\fP + +.TP +\fB\-f\fP | \fB\-\-frequency\fP \fIFREQ\fP +sine wave of \fIFREQ\fP Hz + +.TP +\fB\-\-help\fP +Print usage help + +.TP +\fB\-b\fP | \fB\-\-buffer\fP \fITIME\fP +Use buffer size of \fITIME\fP microseconds. +When 0 is given, use the maximal buffer size. +The default value is 0. + +.TP +\fB\-p\fP | \fB\-\-period\fP \fITIME\fP +Use period size of \fITIME\fP microseconds. +When 0 is given, the periods given by \fB\-P\fP option is used. +The default value is 0. + +.TP +\fB\-P\fP | \fB\-\-nperiods\fP \fIPERIODS\fP +Use number of periods. The default value is 4. + +.TP +\fB\-r\fP | \fB\-\-rate\fP \fIRATE\fP +stream of \fIRATE\fP Hz + +.TP +\fB\-t\fP | \fB\-\-test\fP \fBpink\fP|\fBst2095\fP|\fBsine\fP|\fBwav\fP +\fB\-t pink\fP means use pink noise (default). + +Pink noise is perceptually uniform noise -- that is, it sounds like every frequency at once. If you can hear any tone it may indicate resonances in your speaker system or room. + +\fB\-t st2095\fP means use bandlimited pink noise at -18.5dB AES FS, generated according to SMPTE ST-2095:1-2015. +In addition to speaker localization it may be used for system calibration, for example 85dB for thater drivers, with an extra +10dB for subwoofers. +Per the spec, it is intended "to be used in calibrating the sound pressure level and +electroacoustic response of a cinema B-chain system." +Note that sampling rates less than 48KHz are outside the scope of the spec, and an attempt will be made to construct a reduced rate filter. + +\fB\-t sine\fP means to use sine wave. + +\fB\-t wav\fP means to play WAV files, either pre-defined files or given via \fB\-w\fP option. + +You can pass the number from 1 to 3 as a backward compatibility. + +.TP +\fB\-l\fP | \fB\-\-nloops\fP \fICOUNT\fP + +Specifies the number of loops. Zero means to run infinitely. + +When \fB\-s\fP option below with a valid channel is given, \fBspeaker\-test\fP will perform +always a single-shot without looping. + +.TP +\fB\-s\fP | \fB\-\-speaker\fP \fICHANNEL\fP +Do a single-shot speaker test for the given channel. The channel number starts from 1. +The channel number corresponds to left, right, rear-left, rear-right, center, LFE, +side-left, side-right, and so on. + +For example, when 1 is passed, it tests the left channel only once rather than both channels +with looping. + +.TP +\fB\-w\fP | \fB\-\-wavfile\fP \fIFILE\fP +Use the given WAV file for the playback instead of pre-defined WAV files. + +.TP +\fB\-W\fP | \fB\-\-wavdir\fP \fIDIRECTORY\fP +Specify the directory containing WAV files for playback. +The default path is \fI/usr/share/sounds/alsa\fP. + +.TP +\fB\-m\fP | \fB\-\-chmap\fP \fIMAP\fP +Pass the channel map to override. +If the playback in a specific channel order or channel positions is +required, pass the channel position strings to this option. + +.TP +\fB\-X\fP | \fB\-\-force-frequency\fP +Allow supplied \fIFREQ\fP to be outside the default range of 30-8000Hz. A minimum of 1Hz is still enforced. + +.SH USAGE EXAMPLES + +Produce stereo sound from one stereo jack: +.EX + speaker\-test \-Dplug:front \-c2 +.EE + +Produce 4 speaker sound from two stereo jacks: +.EX + speaker\-test \-Dplug:surround40 \-c4 +.EE + +Produce 5.1 speaker sound from three stereo jacks: +.EX + speaker\-test \-Dplug:surround51 \-c6 +.EE + +To send a nice low 75Hz tone to the Woofer and then exit without touching any other speakers: +.EX + speaker\-test \-Dplug:surround51 \-c6 \-s1 \-f75 +.EE + +To do a 2\-speaker test using the spdif (coax or optical) output: +.EX + speaker\-test \-Dplug:spdif \-c2 +.EE + +Play in the order of front\-right and front-left from the front PCM +.EX + speaker\-test \-Dplug:front \-c2 \-mFR,FL +.EE + +.SH SEE ALSO +.BR aplay(1) + +.SH AUTHOR +The speaker\-test program was written by James Courtier-Dutton. +Pink noise support was added by Nathan Hurst. +Further extensions by Takashi Iwai. +SMPTE ST-2095:1 band-limited pink noise added by Rick Sayre. diff --git a/speaker-test/speaker-test.c b/speaker-test/speaker-test.c new file mode 100644 index 0000000..d3fec2b --- /dev/null +++ b/speaker-test/speaker-test.c @@ -0,0 +1,1316 @@ +/* + * Copyright (C) 2000-2004 James Courtier-Dutton + * Copyright (C) 2005 Nathan Hurst + * + * This file is part of the speaker-test tool. + * + * This small program sends a simple sinusoidal wave to your speakers. + * + * speaker-test is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * speaker-test is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + * + * + * Main program by James Courtier-Dutton (including some source code fragments from the alsa project.) + * Some cleanup from Daniel Caujolle-Bert + * Pink noise option added Nathan Hurst, + * based on generator by Phil Burk (pink.c) + * ST-2095 noise option added Rick Sayre, + * based on generator specified by SMPTE ST-2095:1-2015 + * Also switched to stable harmonic oscillator for sine + * + * Changelog: + * 0.0.9 Added support for ST-2095 band-limited pink noise output, switched to harmonic oscillator for sine + * Changelog: + * 0.0.8 Added support for pink noise output. + * Changelog: + * 0.0.7 Added support for more than 6 channels. + * Changelog: + * 0.0.6 Added support for different sample formats. + * + * $Id: speaker_test.c,v 1.00 2003/11/26 19:43:38 jcdutton Exp $ + */ + +#include "aconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "bswap.h" +#include + +#define ALSA_PCM_NEW_HW_PARAMS_API +#define ALSA_PCM_NEW_SW_PARAMS_API +#include +#include +#include +#include "pink.h" +#include "st2095.h" +#include "gettext.h" +#include "version.h" +#include "os_compat.h" + +#ifdef ENABLE_NLS +#include +#endif + +#ifdef SND_CHMAP_API_VERSION +#define CONFIG_SUPPORT_CHMAP 1 +#endif + +enum { + TEST_PINK_NOISE = 1, + TEST_SINE, + TEST_WAV, + TEST_ST2095_NOISE, + TEST_PATTERN, +}; + +#define MAX_CHANNELS 16 + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24)) +#define LE_SHORT(v) (v) +#define LE_INT(v) (v) +#define BE_SHORT(v) bswap_16(v) +#define BE_INT(v) bswap_32(v) +#else /* __BIG_ENDIAN */ +#define COMPOSE_ID(a,b,c,d) ((d) | ((c)<<8) | ((b)<<16) | ((a)<<24)) +#define LE_SHORT(v) bswap_16(v) +#define LE_INT(v) bswap_32(v) +#define BE_SHORT(v) (v) +#define BE_INT(v) (v) +#endif + +#define ARRAY_SIZE(x) (int)(sizeof(x)/sizeof(x[0])) + +static char *device = "default"; /* playback device */ +static snd_pcm_format_t format = SND_PCM_FORMAT_S16; /* sample format */ +static unsigned int rate = 48000; /* stream rate */ +static unsigned int channels = 1; /* count of channels */ +static unsigned int speaker = 0; /* count of channels */ +static unsigned int buffer_time = 0; /* ring buffer length in us */ +static unsigned int period_time = UINT_MAX; /* period time in us */ +static unsigned int nperiods = 4; /* number of periods */ +static double freq = 440.0; /* sinusoidal wave frequency in Hz */ +static int test_type = TEST_PINK_NOISE; /* Test type. 1 = noise, 2 = sine wave */ +static float generator_scale = 0.8; /* Scale to use for sine volume */ +static snd_pcm_uframes_t buffer_size; +static snd_pcm_uframes_t period_size; +static const char *given_test_wav_file = NULL; +static char *wav_file_dir = SOUNDSDIR; +static int debug = 0; +static int force_frequency = 0; +static int in_aborting = 0; +static snd_pcm_t *pcm_handle = NULL; + +#ifdef CONFIG_SUPPORT_CHMAP +static snd_pcm_chmap_t *channel_map; +static int channel_map_set; +static int *ordered_channels; +#endif + +static const char *const channel_name[MAX_CHANNELS] = { + /* 0 */ N_("Front Left"), + /* 1 */ N_("Front Right"), + /* 2 */ N_("Rear Left"), + /* 3 */ N_("Rear Right"), + /* 4 */ N_("Center"), + /* 5 */ N_("LFE"), + /* 6 */ N_("Side Left"), + /* 7 */ N_("Side Right"), + /* 8 */ N_("Channel 9"), + /* 9 */ N_("Channel 10"), + /* 10 */ N_("Channel 11"), + /* 11 */ N_("Channel 12"), + /* 12 */ N_("Channel 13"), + /* 13 */ N_("Channel 14"), + /* 14 */ N_("Channel 15"), + /* 15 */ N_("Channel 16") +}; + +static const int channels4[] = { + 0, /* Front Left */ + 1, /* Front Right */ + 3, /* Rear Right */ + 2, /* Rear Left */ +}; +static const int channels6[] = { + 0, /* Front Left */ + 4, /* Center */ + 1, /* Front Right */ + 3, /* Rear Right */ + 2, /* Rear Left */ + 5, /* LFE */ +}; +static const int channels8[] = { + 0, /* Front Left */ + 4, /* Center */ + 1, /* Front Right */ + 7, /* Side Right */ + 3, /* Rear Right */ + 2, /* Rear Left */ + 6, /* Side Left */ + 5, /* LFE */ +}; + +#ifdef CONFIG_SUPPORT_CHMAP +/* circular clockwise and bottom-to-top order */ +static const int channel_order[] = { + [SND_CHMAP_FLW] = 10, + [SND_CHMAP_FL] = 20, + [SND_CHMAP_TFL] = 30, + [SND_CHMAP_FLC] = 40, + [SND_CHMAP_TFLC] = 50, + [SND_CHMAP_FC] = 60, + [SND_CHMAP_TFC] = 70, + [SND_CHMAP_FRC] = 80, + [SND_CHMAP_TFRC] = 90, + [SND_CHMAP_FR] = 100, + [SND_CHMAP_TFR] = 110, + [SND_CHMAP_FRW] = 120, + [SND_CHMAP_SR] = 130, + [SND_CHMAP_TSR] = 140, + [SND_CHMAP_RR] = 150, + [SND_CHMAP_TRR] = 160, + [SND_CHMAP_RRC] = 170, + [SND_CHMAP_RC] = 180, + [SND_CHMAP_TRC] = 190, + [SND_CHMAP_RLC] = 200, + [SND_CHMAP_RL] = 210, + [SND_CHMAP_TRL] = 220, + [SND_CHMAP_SL] = 230, + [SND_CHMAP_TSL] = 240, + [SND_CHMAP_BC] = 250, + [SND_CHMAP_TC] = 260, + [SND_CHMAP_LLFE] = 270, + [SND_CHMAP_LFE] = 280, + [SND_CHMAP_RLFE] = 290, + /* not in table = 10000 */ + [SND_CHMAP_UNKNOWN] = 20000, + [SND_CHMAP_NA] = 30000, +}; + +static int chpos_cmp(const void *chnum1p, const void *chnum2p) +{ + int chnum1 = *(int *)chnum1p; + int chnum2 = *(int *)chnum2p; + int chpos1 = channel_map->pos[chnum1]; + int chpos2 = channel_map->pos[chnum2]; + int weight1 = 10000; + int weight2 = 10000; + + if (chpos1 < ARRAY_SIZE(channel_order) && channel_order[chpos1]) + weight1 = channel_order[chpos1]; + if (chpos2 < ARRAY_SIZE(channel_order) && channel_order[chpos2]) + weight2 = channel_order[chpos2]; + + if (weight1 == weight2) { + /* order by channel number if both have the same position (e.g. UNKNOWN) + * or if neither is in channel_order[] */ + return chnum1 - chnum2; + } + + /* order according to channel_order[] */ + return weight1 - weight2; +} + +static int *order_channels(void) +{ + /* create a (playback order => channel number) table with channels ordered + * according to channel_order[] values */ + unsigned int i; + int *ordered_chs; + + ordered_chs = calloc(channel_map->channels, sizeof(*ordered_chs)); + if (!ordered_chs) + return NULL; + + for (i = 0; i < channel_map->channels; i++) + ordered_chs[i] = i; + + qsort(ordered_chs, channel_map->channels, sizeof(*ordered_chs), chpos_cmp); + + return ordered_chs; +} +#endif + +static int get_speaker_channel(int chn) +{ +#ifdef CONFIG_SUPPORT_CHMAP + if (channel_map_set || (ordered_channels && (unsigned int)chn >= channel_map->channels)) + return chn; + if (ordered_channels) + return ordered_channels[chn]; +#endif + + switch (channels) { + case 4: + chn = channels4[chn]; + break; + case 6: + chn = channels6[chn]; + break; + case 8: + chn = channels8[chn]; + break; + } + + return chn; +} + +static const char *get_channel_name(int chn) +{ +#ifdef CONFIG_SUPPORT_CHMAP + if (channel_map) { + const char *name = NULL; + if ((unsigned int)chn < channel_map->channels) + name = snd_pcm_chmap_long_name(channel_map->pos[chn]); + return name ? name : "Unknown"; + } +#endif + return gettext(channel_name[chn]); +} + +static const int supported_formats[] = { + SND_PCM_FORMAT_S8, + SND_PCM_FORMAT_S16_LE, + SND_PCM_FORMAT_S16_BE, + SND_PCM_FORMAT_FLOAT_LE, + SND_PCM_FORMAT_S24_3LE, + SND_PCM_FORMAT_S24_3BE, + SND_PCM_FORMAT_S24_LE, + SND_PCM_FORMAT_S24_BE, + SND_PCM_FORMAT_S32_LE, + SND_PCM_FORMAT_S32_BE, + -1 +}; + +typedef union { + float f; + int32_t i; +} value_t; + +static void do_generate(uint8_t *frames, int channel, int count, + value_t (*generate)(void *), void *arg) +{ + value_t res; + unsigned int chn; + int8_t *samp8 = (int8_t*) frames; + int16_t *samp16 = (int16_t*) frames; + int32_t *samp32 = (int32_t*) frames; + float *samp_f = (float*) frames; + + while (count-- > 0) { + for(chn=0;chn> 24; + break; + case SND_PCM_FORMAT_S16_LE: + *samp16++ = LE_SHORT(res.i >> 16); + break; + case SND_PCM_FORMAT_S16_BE: + *samp16++ = BE_SHORT(res.i >> 16); + break; + case SND_PCM_FORMAT_FLOAT_LE: + *samp_f++ = res.f; + break; + case SND_PCM_FORMAT_S24_3LE: + res.i >>= 8; + *samp8++ = LE_INT(res.i); + *samp8++ = LE_INT(res.i) >> 8; + *samp8++ = LE_INT(res.i) >> 16; + break; + case SND_PCM_FORMAT_S24_3BE: + res.i >>= 8; + *samp8++ = BE_INT(res.i); + *samp8++ = BE_INT(res.i) >> 8; + *samp8++ = BE_INT(res.i) >> 16; + break; + case SND_PCM_FORMAT_S24_LE: + res.i >>= 8; + *samp8++ = LE_INT(res.i); + *samp8++ = LE_INT(res.i) >> 8; + *samp8++ = LE_INT(res.i) >> 16; + *samp8++ = 0; + break; + case SND_PCM_FORMAT_S24_BE: + res.i >>= 8; + *samp8++ = 0; + *samp8++ = BE_INT(res.i); + *samp8++ = BE_INT(res.i) >> 8; + *samp8++ = BE_INT(res.i) >> 16; + break; + case SND_PCM_FORMAT_S32_LE: + *samp32++ = LE_INT(res.i); + break; + case SND_PCM_FORMAT_S32_BE: + *samp32++ = BE_INT(res.i); + break; + default: + ; + } + } + } +} + +/* + * Sine generator + */ +typedef struct { + double a; + double s; + double c; +} sine_t; + +static void init_sine(sine_t *sine) +{ + // symplectic integration for fast, stable harmonic oscillator + sine->a = 2.0*M_PI * freq / rate; + sine->c = 1.0; + sine->s = 0.0; +} + +static value_t generate_sine(void *arg) +{ + sine_t *sine = arg; + value_t res; + + res.f = sine->s * generator_scale; + if (format != SND_PCM_FORMAT_FLOAT_LE) + res.i = res.f * INT32_MAX; + + // update the oscillator + sine->c -= sine->a * sine->s; + sine->s += sine->a * sine->c; + return res; +} + +/* Pink noise is a better test than sine wave because we can tell + * where pink noise is coming from more easily that a sine wave. + */ +static value_t generate_pink_noise(void *arg) +{ + pink_noise_t *pink = arg; + value_t res; + + res.f = generate_pink_noise_sample(pink) * generator_scale; + if (format != SND_PCM_FORMAT_FLOAT_LE) + res.i = res.f * INT32_MAX; + return res; +} + +/* Band-Limited Pink Noise, per SMPTE ST 2095-1 + * beyond speaker localization, this can be used for setting loudness to standard + */ +static value_t generate_st2095_noise(void *arg) +{ + st2095_noise_t *st2095 = arg; + value_t res; + + res.f = generate_st2095_noise_sample(st2095); + if (format != SND_PCM_FORMAT_FLOAT_LE) + res.i = res.f * INT32_MAX; + return res; +} + +/* + * useful for tests + */ +static value_t generate_pattern(void *arg) +{ + value_t res; + + res.i = *(int *)arg; + *(int *)arg = res.i + 1; + if (format != SND_PCM_FORMAT_FLOAT_LE) + res.f = (float)res.i / (float)INT32_MAX; + return res; +} + +static int set_hwparams(snd_pcm_t *handle, snd_pcm_hw_params_t *params, snd_pcm_access_t access) { + unsigned int rrate; + int err; + snd_pcm_uframes_t period_size_min; + snd_pcm_uframes_t period_size_max; + snd_pcm_uframes_t buffer_size_min; + snd_pcm_uframes_t buffer_size_max; + + /* choose all parameters */ + err = snd_pcm_hw_params_any(handle, params); + if (err < 0) { + fprintf(stderr, _("Broken configuration for playback: no configurations available: %s\n"), snd_strerror(err)); + return err; + } + + /* set the interleaved read/write format */ + err = snd_pcm_hw_params_set_access(handle, params, access); + if (err < 0) { + fprintf(stderr, _("Access type not available for playback: %s\n"), snd_strerror(err)); + return err; + } + + /* set the sample format */ + err = snd_pcm_hw_params_set_format(handle, params, format); + if (err < 0) { + fprintf(stderr, _("Sample format not available for playback: %s\n"), snd_strerror(err)); + return err; + } + + /* set the count of channels */ + err = snd_pcm_hw_params_set_channels(handle, params, channels); + if (err < 0) { + fprintf(stderr, _("Channels count (%i) not available for playbacks: %s\n"), channels, snd_strerror(err)); + return err; + } + + /* set the stream rate */ + rrate = rate; + err = snd_pcm_hw_params_set_rate(handle, params, rate, 0); + if (err < 0) { + fprintf(stderr, _("Rate %iHz not available for playback: %s\n"), rate, snd_strerror(err)); + return err; + } + + if (rrate != rate) { + fprintf(stderr, _("Rate doesn't match (requested %iHz, get %iHz, err %d)\n"), rate, rrate, err); + return -EINVAL; + } + + printf(_("Rate set to %iHz (requested %iHz)\n"), rrate, rate); + /* set the buffer time */ + err = snd_pcm_hw_params_get_buffer_size_min(params, &buffer_size_min); + err = snd_pcm_hw_params_get_buffer_size_max(params, &buffer_size_max); + err = snd_pcm_hw_params_get_period_size_min(params, &period_size_min, NULL); + err = snd_pcm_hw_params_get_period_size_max(params, &period_size_max, NULL); + printf(_("Buffer size range from %lu to %lu\n"),buffer_size_min, buffer_size_max); + printf(_("Period size range from %lu to %lu\n"),period_size_min, period_size_max); + if (period_time > 0) { + unsigned int tmp = period_time; + if (period_time > 0 && period_time < UINT_MAX) + printf(_("Requested period time %u us\n"), period_time); + else + tmp = 250000; /* 0.25 second */ + err = snd_pcm_hw_params_set_period_time_near(handle, params, &tmp, NULL); + if (err < 0) { + fprintf(stderr, _("Unable to set period time %u us for playback: %s\n"), + tmp, snd_strerror(err)); + return err; + } + } + if (buffer_time > 0) { + printf(_("Requested buffer time %u us\n"), buffer_time); + err = snd_pcm_hw_params_set_buffer_time_near(handle, params, &buffer_time, NULL); + if (err < 0) { + fprintf(stderr, _("Unable to set buffer time %u us for playback: %s\n"), + buffer_time, snd_strerror(err)); + return err; + } + } + if (! buffer_time && ! period_time) { + buffer_size = buffer_size_max; + if (! period_time) + buffer_size = (buffer_size / nperiods) * nperiods; + printf(_("Using max buffer size %lu\n"), buffer_size); + err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size); + if (err < 0) { + fprintf(stderr, _("Unable to set buffer size %lu for playback: %s\n"), + buffer_size, snd_strerror(err)); + return err; + } + } + if (! buffer_time || ! period_time) { + printf(_("Periods = %u\n"), nperiods); + err = snd_pcm_hw_params_set_periods_near(handle, params, &nperiods, NULL); + if (err < 0) { + fprintf(stderr, _("Unable to set nperiods %u for playback: %s\n"), + nperiods, snd_strerror(err)); + return err; + } + } + + /* write the parameters to device */ + err = snd_pcm_hw_params(handle, params); + if (err < 0) { + fprintf(stderr, _("Unable to set hw params for playback: %s\n"), snd_strerror(err)); + return err; + } + + snd_pcm_hw_params_get_buffer_size(params, &buffer_size); + snd_pcm_hw_params_get_period_size(params, &period_size, NULL); + printf(_("was set period_size = %lu\n"),period_size); + printf(_("was set buffer_size = %lu\n"),buffer_size); + if (2*period_size > buffer_size) { + fprintf(stderr, _("buffer to small, could not use\n")); + return -EINVAL; + } + + return 0; +} + +static int set_swparams(snd_pcm_t *handle, snd_pcm_sw_params_t *swparams) { + int err; + + /* get the current swparams */ + err = snd_pcm_sw_params_current(handle, swparams); + if (err < 0) { + fprintf(stderr, _("Unable to determine current swparams for playback: %s\n"), snd_strerror(err)); + return err; + } + + /* start the transfer when a buffer is full */ + err = snd_pcm_sw_params_set_start_threshold(handle, swparams, buffer_size); + if (err < 0) { + fprintf(stderr, _("Unable to set start threshold mode for playback: %s\n"), snd_strerror(err)); + return err; + } + + /* allow the transfer when at least period_size frames can be processed */ + err = snd_pcm_sw_params_set_avail_min(handle, swparams, period_size); + if (err < 0) { + fprintf(stderr, _("Unable to set avail min for playback: %s\n"), snd_strerror(err)); + return err; + } + + /* write the parameters to the playback device */ + err = snd_pcm_sw_params(handle, swparams); + if (err < 0) { + fprintf(stderr, _("Unable to set sw params for playback: %s\n"), snd_strerror(err)); + return err; + } + + return 0; +} + +#ifdef CONFIG_SUPPORT_CHMAP +static int config_chmap(snd_pcm_t *handle, const char *mapstr) +{ + int err; + + if (mapstr) { + channel_map = snd_pcm_chmap_parse_string(mapstr); + if (!channel_map) { + fprintf(stderr, _("Unable to parse channel map string: %s\n"), mapstr); + return -EINVAL; + } + err = snd_pcm_set_chmap(handle, channel_map); + if (err < 0) { + fprintf(stderr, _("Unable to set channel map: %s\n"), mapstr); + return err; + } + channel_map_set = 1; + return 0; + } + + channel_map = snd_pcm_get_chmap(handle); + + /* create a channel order table for default layouts */ + if (channel_map) + ordered_channels = order_channels(); + + return 0; +} +#endif + +/* + * Underrun and suspend recovery + */ + +static int xrun_recovery(snd_pcm_t *handle, int err) { + if (err == -EPIPE) { /* under-run */ + err = snd_pcm_prepare(handle); + if (err < 0) + fprintf(stderr, _("Can't recovery from underrun, prepare failed: %s\n"), snd_strerror(err)); + return 0; + } + else if (err == -ESTRPIPE) { + + while ((err = snd_pcm_resume(handle)) == -EAGAIN) + sleep(1); /* wait until the suspend flag is released */ + + if (err < 0) { + err = snd_pcm_prepare(handle); + if (err < 0) + fprintf(stderr, _("Can't recovery from suspend, prepare failed: %s\n"), snd_strerror(err)); + } + + return 0; + } + + return err; +} + +/* + * Handle WAV files + */ + +static const char *wav_file[MAX_CHANNELS]; +static int wav_file_size[MAX_CHANNELS]; + +struct wave_header { + struct { + uint32_t magic; + uint32_t length; + uint32_t type; + } hdr; + struct { + uint32_t type; + uint32_t length; + } chunk1; + struct { + uint16_t format; + uint16_t channels; + uint32_t rate; + uint32_t bytes_per_sec; + uint16_t sample_size; + uint16_t sample_bits; + } body; + struct { + uint32_t type; + uint32_t length; + } chunk; +}; + +#define WAV_RIFF COMPOSE_ID('R','I','F','F') +#define WAV_WAVE COMPOSE_ID('W','A','V','E') +#define WAV_FMT COMPOSE_ID('f','m','t',' ') +#define WAV_DATA COMPOSE_ID('d','a','t','a') +#define WAV_PCM_CODE 1 + +static const char *search_for_file(const char *name) +{ + char *file; + if (*name == '/') + return strdup(name); + file = malloc(strlen(wav_file_dir) + strlen(name) + 2); + if (file) + sprintf(file, "%s/%s", wav_file_dir, name); + return file; +} + +static int check_wav_file(int channel, const char *name) +{ + struct wave_header header; + int fd; + + wav_file[channel] = search_for_file(name); + if (! wav_file[channel]) { + fprintf(stderr, _("No enough memory\n")); + return -ENOMEM; + } + + if ((fd = open(wav_file[channel], O_RDONLY)) < 0) { + fprintf(stderr, _("Cannot open WAV file %s\n"), wav_file[channel]); + return -EINVAL; + } + if (read(fd, &header, sizeof(header)) < (int)sizeof(header)) { + fprintf(stderr, _("Invalid WAV file %s\n"), wav_file[channel]); + goto error; + } + + if (header.hdr.magic != WAV_RIFF || header.hdr.type != WAV_WAVE) { + fprintf(stderr, _("Not a WAV file: %s\n"), wav_file[channel]); + goto error; + } + if (header.body.format != LE_SHORT(WAV_PCM_CODE)) { + fprintf(stderr, _("Unsupported WAV format %d for %s\n"), + LE_SHORT(header.body.format), wav_file[channel]); + goto error; + } + if (header.body.channels != LE_SHORT(1)) { + fprintf(stderr, _("%s is not a mono stream (%d channels)\n"), + wav_file[channel], LE_SHORT(header.body.channels)); + goto error; + } + if (header.body.rate != LE_INT(rate)) { + fprintf(stderr, _("Sample rate doesn't match (%d) for %s\n"), + LE_INT(header.body.rate), wav_file[channel]); + goto error; + } + if (header.body.sample_bits != LE_SHORT(16)) { + fprintf(stderr, _("Unsupported sample format bits %d for %s\n"), + LE_SHORT(header.body.sample_bits), wav_file[channel]); + goto error; + } + if (header.chunk.type != WAV_DATA) { + fprintf(stderr, _("Invalid WAV file %s\n"), wav_file[channel]); + goto error; + } + wav_file_size[channel] = LE_INT(header.chunk.length); + close(fd); + return 0; + + error: + close(fd); + return -EINVAL; +} + +static int setup_wav_file(int chn) +{ + static const char *const wavs[MAX_CHANNELS] = { + "Front_Left.wav", + "Front_Right.wav", + "Rear_Left.wav", + "Rear_Right.wav", + "Front_Center.wav", + "Rear_Center.wav", /* FIXME: should be "Bass" or so */ + "Side_Left.wav", + "Side_Right.wav", + "Channel_9.wav", + "Channel_10.wav", + "Channel_11.wav", + "Channel_12.wav", + "Channel_13.wav", + "Channel_14.wav", + "Channel_15.wav", + "Channel_16.wav" + }; + + if (given_test_wav_file) + return check_wav_file(chn, given_test_wav_file); + +#ifdef CONFIG_SUPPORT_CHMAP + if (channel_map && (unsigned int)chn < channel_map->channels) { + int channel = channel_map->pos[chn] - SND_CHMAP_FL; + if (channel >= 0 && channel < MAX_CHANNELS) + return check_wav_file(chn, wavs[channel]); + } +#endif + + return check_wav_file(chn, wavs[chn]); +} + +static int read_wav(uint16_t *buf, int channel, int offset, int bufsize) +{ + static FILE *wavfp = NULL; + int size; + + if (in_aborting) + return -EFAULT; + + if (! wav_file[channel]) { + fprintf(stderr, _("Undefined channel %d\n"), channel); + return -EINVAL; + } + + if (offset >= wav_file_size[channel]) + return 0; /* finished */ + + if (! offset) { + if (wavfp) + fclose(wavfp); + wavfp = fopen(wav_file[channel], "r"); + if (! wavfp) + return -errno; + if (fseek(wavfp, sizeof(struct wave_header), SEEK_SET) < 0) + return -errno; + } + if (offset + bufsize > wav_file_size[channel]) + bufsize = wav_file_size[channel] - offset; + bufsize /= channels; + for (size = 0; size < bufsize; size += 2) { + unsigned int chn; + for (chn = 0; chn < channels; chn++) { + if (chn == (unsigned int)channel) { + if (fread(buf, 2, 1, wavfp) != 1) + return size; + } + else + *buf = 0; + buf++; + } + } + return size; +} + + +/* + * Transfer method - write only + */ + +static int write_buffer(snd_pcm_t *handle, uint8_t *ptr, int cptr) +{ + int err; + + while (cptr > 0 && !in_aborting) { + + err = snd_pcm_writei(handle, ptr, cptr); + + if (err == -EAGAIN) + continue; + + if (err < 0) { + fprintf(stderr, _("Write error: %d,%s\n"), err, snd_strerror(err)); + if ((err = xrun_recovery(handle, err)) < 0) { + fprintf(stderr, _("xrun_recovery failed: %d,%s\n"), err, snd_strerror(err)); + return err; + } + break; /* skip one period */ + } + + ptr += snd_pcm_frames_to_bytes(handle, err); + cptr -= err; + } + return 0; +} + +static int pattern; +static sine_t sine; +static pink_noise_t pink; +static st2095_noise_t st2095; + +static void init_loop(void) +{ + switch (test_type) { + case TEST_ST2095_NOISE: + initialize_st2095_noise(&st2095, rate); + break; + case TEST_PINK_NOISE: + initialize_pink_noise(&pink, 16); + break; + case TEST_SINE: + init_sine(&sine); + break; + case TEST_PATTERN: + pattern = 0; + break; + } +} + +static int write_loop(snd_pcm_t *handle, int channel, int periods, uint8_t *frames) +{ + unsigned int cnt; + int n; + int err; + + fflush(stdout); + if (test_type == TEST_WAV) { + int bufsize = snd_pcm_frames_to_bytes(handle, period_size); + cnt = 0; + while ((err = read_wav((uint16_t *)frames, channel, cnt, bufsize)) > 0 && !in_aborting) { + cnt += err; + if ((err = write_buffer(handle, frames, + snd_pcm_bytes_to_frames(handle, err * channels))) < 0) + break; + } + if (buffer_size > cnt && !in_aborting) { + snd_pcm_drain(handle); + snd_pcm_prepare(handle); + } + return err; + } + + + if (periods <= 0) + periods = 1; + + for(n = 0; n < periods && !in_aborting; n++) { + if (test_type == TEST_PINK_NOISE) + do_generate(frames, channel, period_size, generate_pink_noise, &pink); + else if (test_type == TEST_PATTERN) + do_generate(frames, channel, period_size, generate_pattern, &pattern); + else if (test_type == TEST_ST2095_NOISE) { + reset_st2095_noise_measurement(&st2095); + do_generate(frames, channel, period_size, generate_st2095_noise, &st2095); + printf(_("\tSMPTE ST-2095 noise batch was %2.2fdB RMS\n"), + compute_st2095_noise_measurement(&st2095, period_size)); + } else + do_generate(frames, channel, period_size, generate_sine, &sine); + + if ((err = write_buffer(handle, frames, period_size)) < 0) + return err; + } + if (buffer_size > n * period_size && !in_aborting) { + snd_pcm_drain(handle); + snd_pcm_prepare(handle); + } + return 0; +} + +static int prg_exit(int code) +{ + if (pcm_handle) + snd_pcm_close(pcm_handle); + exit(code); + return code; +} + +static void signal_handler(int sig) +{ + if (in_aborting) + return; + + in_aborting = 1; + + if (pcm_handle) + snd_pcm_abort(pcm_handle); + if (sig == SIGABRT) { + pcm_handle = NULL; + prg_exit(EXIT_FAILURE); + } + signal(sig, signal_handler); +} + +static void help(void) +{ + const int *fmt; + + printf( + _("Usage: speaker-test [OPTION]... \n" + "-h,--help help\n" + "-D,--device playback device\n" + "-r,--rate stream rate in Hz\n" + "-c,--channels count of channels in stream\n" + "-f,--frequency sine wave frequency in Hz\n" + "-F,--format sample format\n" + "-b,--buffer ring buffer size in us\n" + "-p,--period period size in us\n" + "-P,--nperiods number of periods\n" + "-t,--test pink=use pink noise, sine=use sine wave, st2095=use SMPTE ST-2095 noise, wav=WAV file\n" + "-l,--nloops specify number of loops to test, 0 = infinite\n" + "-s,--speaker single speaker test. Values 1=Left, 2=right, etc\n" + "-w,--wavfile Use the given WAV file as a test sound\n" + "-W,--wavdir Specify the directory containing WAV files\n" + "-m,--chmap Specify the channel map to override\n" + "-X,--force-frequency force frequencies outside the 30-8000hz range\n" + "-S,--scale Scale of generated test tones in percent (default=80)\n" + "\n")); + printf(_("Recognized sample formats are:")); + for (fmt = supported_formats; *fmt >= 0; fmt++) { + const char *s = snd_pcm_format_name(*fmt); + if (s) + printf(" %s", s); + } + + printf("\n\n"); +} + +int main(int argc, char *argv[]) { + snd_pcm_t *handle; + int err, morehelp; + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; + uint8_t *frames; + unsigned int chn; + const int *fmt; + double time1,time2,time3; + unsigned int n, nloops; + struct timeval tv1,tv2; + int speakeroptset = 0; +#ifdef CONFIG_SUPPORT_CHMAP + const char *chmap = NULL; +#endif + + static const struct option long_option[] = { + {"help", 0, NULL, 'h'}, + {"device", 1, NULL, 'D'}, + {"rate", 1, NULL, 'r'}, + {"channels", 1, NULL, 'c'}, + {"frequency", 1, NULL, 'f'}, + {"format", 1, NULL, 'F'}, + {"buffer", 1, NULL, 'b'}, + {"period", 1, NULL, 'p'}, + {"nperiods", 1, NULL, 'P'}, + {"test", 1, NULL, 't'}, + {"nloops", 1, NULL, 'l'}, + {"speaker", 1, NULL, 's'}, + {"wavfile", 1, NULL, 'w'}, + {"wavdir", 1, NULL, 'W'}, + {"debug", 0, NULL, 'd'}, + {"force-frequency", 0, NULL, 'X'}, + {"scale", 1, NULL, 'S'}, +#ifdef CONFIG_SUPPORT_CHMAP + {"chmap", 1, NULL, 'm'}, +#endif + {NULL, 0, NULL, 0 }, + }; + +#ifdef ENABLE_NLS + setlocale(LC_ALL, ""); + textdomain(PACKAGE); +#endif + + snd_pcm_hw_params_alloca(&hwparams); + snd_pcm_sw_params_alloca(&swparams); + + nloops = 0; + morehelp = 0; + + printf("\nspeaker-test %s\n\n", SND_UTIL_VERSION_STR); + while (1) { + int c; + + if ((c = getopt_long(argc, argv, "hD:r:c:f:F:b:p:P:t:l:s:w:W:d:XS:" +#ifdef CONFIG_SUPPORT_CHMAP + "m:" +#endif + , long_option, NULL)) < 0) + break; + + switch (c) { + case 'h': + morehelp++; + break; + case 'D': + device = strdup(optarg); + break; + case 'F': + format = snd_pcm_format_value(optarg); + for (fmt = supported_formats; *fmt >= 0; fmt++) + if (*fmt == format) + break; + if (*fmt < 0) { + fprintf(stderr, "Format %s is not supported...\n", snd_pcm_format_name(format)); + exit(EXIT_FAILURE); + } + break; + case 'r': + rate = atoi(optarg); + rate = rate < 4000 ? 4000 : rate; + rate = rate > 768000 ? 768000 : rate; + break; + case 'c': + channels = atoi(optarg); + channels = channels < 1 ? 1 : channels; + channels = channels > 1024 ? 1024 : channels; + break; + case 'f': + freq = atof(optarg); + break; + case 'b': + buffer_time = atoi(optarg); + buffer_time = buffer_time > 100000000 ? 100000000 : buffer_time; + break; + case 'p': + period_time = atoi(optarg); + period_time = period_time > 100000000 ? 100000000 : period_time; + break; + case 'P': + nperiods = atoi(optarg); + if (nperiods < 2 || nperiods > 1024) { + fprintf(stderr, _("Invalid number of periods %d\n"), nperiods); + exit(1); + } + break; + case 't': + if (*optarg == 'p') + test_type = TEST_PINK_NOISE; + else if (*optarg == 's') { + if (optarg[1] == 'i') + test_type = TEST_SINE; + else if (optarg[1] == 't') + test_type = TEST_ST2095_NOISE; + else { + fprintf(stderr, _("Invalid test type %s\n"), optarg); + exit(1); + } + } else if (*optarg == 'w') + test_type = TEST_WAV; + else if (*optarg == 't') + test_type = TEST_PATTERN; + else if (isdigit(*optarg)) { + test_type = atoi(optarg); + if (test_type < TEST_PINK_NOISE || test_type > TEST_PATTERN) { + fprintf(stderr, _("Invalid test type %s\n"), optarg); + exit(1); + } + } else { + fprintf(stderr, _("Invalid test type %s\n"), optarg); + exit(1); + } + break; + case 'l': + nloops = atoi(optarg); + break; + case 's': + speaker = atoi(optarg); + speaker = speaker < 1 ? 0 : speaker; + speakeroptset = 1; + break; + case 'w': + given_test_wav_file = optarg; + break; + case 'W': + wav_file_dir = optarg; + break; + case 'd': + debug = 1; + break; + case 'X': + force_frequency = 1; + break; +#ifdef CONFIG_SUPPORT_CHMAP + case 'm': + chmap = optarg; + break; +#endif + case 'S': + generator_scale = atoi(optarg) / 100.0; + break; + default: + fprintf(stderr, _("Unknown option '%c'\n"), c); + exit(EXIT_FAILURE); + break; + } + } + + if (morehelp) { + help(); + exit(EXIT_SUCCESS); + } + + if (speakeroptset) { + speaker = speaker > channels ? 0 : speaker; + if (speaker==0) { + fprintf(stderr, _("Invalid parameter for -s option.\n")); + exit(EXIT_FAILURE); + } + } + + if (!force_frequency) { + freq = freq < 30.0 ? 30.0 : freq; + freq = freq > 8000.0 ? 8000.0 : freq; + } else { + freq = freq < 1.0 ? 1.0 : freq; + } + + if (test_type == TEST_WAV) + format = SND_PCM_FORMAT_S16_LE; /* fixed format */ + + printf(_("Playback device is %s\n"), device); + printf(_("Stream parameters are %iHz, %s, %i channels\n"), rate, snd_pcm_format_name(format), channels); + switch (test_type) { + case TEST_ST2095_NOISE: + printf(_("Using SMPTE ST-2095 -18.5dB AES FS band-limited pink noise\n")); + break; + case TEST_PINK_NOISE: + printf(_("Using 16 octaves of pink noise\n")); + break; + case TEST_SINE: + printf(_("Sine wave rate is %.4fHz\n"), freq); + break; + case TEST_WAV: + printf(_("WAV file(s)\n")); + break; + + } + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + signal(SIGABRT, signal_handler); + + if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + printf(_("Playback open error: %d,%s\n"), err,snd_strerror(err)); + prg_exit(EXIT_FAILURE); + } + pcm_handle = handle; + + if ((err = set_hwparams(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + printf(_("Setting of hwparams failed: %s\n"), snd_strerror(err)); + prg_exit(EXIT_FAILURE); + } + if ((err = set_swparams(handle, swparams)) < 0) { + printf(_("Setting of swparams failed: %s\n"), snd_strerror(err)); + prg_exit(EXIT_FAILURE); + } + +#ifdef CONFIG_SUPPORT_CHMAP + err = config_chmap(handle, chmap); + if (err < 0) + prg_exit(EXIT_FAILURE); +#endif + + if (debug) { + snd_output_t *log; + err = snd_output_stdio_attach(&log, stderr, 0); + if (err >= 0) { + snd_pcm_dump(handle, log); + snd_output_close(log); + } + } + + frames = malloc(snd_pcm_frames_to_bytes(handle, period_size)); + if (frames == NULL) { + fprintf(stderr, _("No enough memory\n")); + prg_exit(EXIT_FAILURE); + } + + init_loop(); + + if (speaker==0) { + + if (test_type == TEST_WAV) { + for (chn = 0; chn < channels; chn++) { + if (setup_wav_file(get_speaker_channel(chn)) < 0) + prg_exit(EXIT_FAILURE); + } + } + + for (n = 0; (! nloops || n < nloops) && !in_aborting; n++) { + + gettimeofday(&tv1, NULL); + for(chn = 0; chn < channels; chn++) { + int channel = get_speaker_channel(chn); + printf(" %d - %s\n", channel, get_channel_name(channel)); + + err = write_loop(handle, channel, ((rate*3)/period_size), frames); + + if (err < 0) { + fprintf(stderr, _("Transfer failed: %s\n"), snd_strerror(err)); + prg_exit(EXIT_SUCCESS); + } + } + gettimeofday(&tv2, NULL); + time1 = (double)tv1.tv_sec + ((double)tv1.tv_usec / 1000000.0); + time2 = (double)tv2.tv_sec + ((double)tv2.tv_usec / 1000000.0); + time3 = time2 - time1; + printf(_("Time per period = %lf\n"), time3 ); + } + } else { + chn = get_speaker_channel(speaker - 1); + + if (test_type == TEST_WAV) { + if (setup_wav_file(chn) < 0) + prg_exit(EXIT_FAILURE); + } + + printf(" - %s\n", get_channel_name(chn)); + err = write_loop(handle, chn, ((rate*5)/period_size), frames); + + if (err < 0) { + fprintf(stderr, _("Transfer failed: %s\n"), snd_strerror(err)); + } + } + + snd_pcm_drain(handle); + + free(frames); +#ifdef CONFIG_SUPPORT_CHMAP + free(ordered_channels); +#endif + + return prg_exit(EXIT_SUCCESS); +} diff --git a/speaker-test/st2095.c b/speaker-test/st2095.c new file mode 100644 index 0000000..f036074 --- /dev/null +++ b/speaker-test/st2095.c @@ -0,0 +1,164 @@ +/* + st2095.c + + Generate Bandlimited Pink Noise (-18.5dB AES FS) + Using the SMPTE ST 2095:1-2015 standard + + Based on pseudo-code from the above SMPTE standard, which bore the credit + "Revised 2015-01-04 by Calvert Dayton" + + Copyleft 2023 Rick Sayre - No rights reserved. +*/ + +#include "aconfig.h" +#include +#include +#include "st2095.h" + +/************************************************************/ + + +void reset_st2095_noise_measurement( st2095_noise_t *st2095 ) { + st2095->accum = 0.; +} + +float compute_st2095_noise_measurement( st2095_noise_t *st2095, int period ) { + return(10. * log10f(st2095->accum / (float)period) + 3.01); +} + +void initialize_st2095_noise( st2095_noise_t *st2095, int sample_rate) { + // Periodicity in samples must be a power of two, <= 2^31 + // Typical values are 524288, 1048576, 2097152 or 4194304 + if (sample_rate > 48000) { + // Special case LCG step for 1024K samples @ 88.2K or 96k + st2095->samplesPerPeriod = 1048576; + st2095->randStep = 163841; + } else { + st2095->samplesPerPeriod = 524288; + st2095->randStep = 52737; + } + + // set up LCG PRNG + st2095->randMax = st2095->samplesPerPeriod - 1; + st2095->seed = 0; + st2095->scaleFactor = 2.0 / (float)st2095->randMax; + + st2095->maxAmp = powf(10.0, ST2095_MAX_PEAK / 20.0); + + // Calculate omegaT for matched Z transform highpass filters + st2095->w0t = 2.0 * M_PI * ST2095_HPFC / (float)sample_rate; + + // Limit LpFc <= Nyquist (actually lower, based on 48 vs 22.4 KHz spec cutoff) + // The spec says the filter begins at 22.4KHz, if we ask for a Nyquist-impossible + // sampling rate, compute something with the same relationship + st2095->LpFc = ST2095_LPFC; + float rateratio = 48000. / ST2095_LPFC; + if (st2095->LpFc > sample_rate/rateratio) + st2095->LpFc = sample_rate/rateratio; + + // Calculate k and k^2 for bilinear transform lowpass filters + st2095->k = tanf(( 2.0 * M_PI * st2095->LpFc / (float)sample_rate ) / 2.0); + st2095->k2 = st2095->k * st2095->k; + + // Calculate biquad coefficients for bandpass filter components + st2095->hp1_a1 = -2.0 * expf(-0.3826835 * st2095->w0t) * cosf(0.9238795 * st2095->w0t); + st2095->hp1_a2 = expf(2.0 * -0.3826835 * st2095->w0t); + st2095->hp1_b0 = (1.0 - st2095->hp1_a1 + st2095->hp1_a2) / 4.0; + st2095->hp1_b1 = -2.0 * st2095->hp1_b0; + st2095->hp1_b2 = st2095->hp1_b0; + + st2095->hp2_a1 = -2.0 * expf(-0.9238795 * st2095->w0t) * cosf(0.3826835 * st2095->w0t); + st2095->hp2_a2 = expf(2.0 * -0.9238795 * st2095->w0t); + st2095->hp2_b0 = (1.0 - st2095->hp2_a1 + st2095->hp2_a2) / 4.0; + st2095->hp2_b1 = -2.0 * st2095->hp2_b0; + st2095->hp2_b2 = st2095->hp2_b0; + + st2095->lp1_a1 = (2.0 * (st2095->k2 - 1.0)) / (st2095->k2 + (st2095->k / 1.306563) + 1.0); + st2095->lp1_a2 = (st2095->k2 - (st2095->k / 1.306563) + 1.0) / (st2095->k2 + (st2095->k / 1.306563) + 1.0); + st2095->lp1_b0 = st2095->k2 / (st2095->k2 + (st2095->k / 1.306563) + 1.0); + st2095->lp1_b1 = 2.0 * st2095->lp1_b0; + st2095->lp1_b2 = st2095->lp1_b0; + + st2095->lp2_a1 = (2.0 * (st2095->k2 - 1.0)) / (st2095->k2 + (st2095->k / 0.541196) + 1.0); + st2095->lp2_a2 = (st2095->k2 - (st2095->k / 0.541196) + 1.0) / (st2095->k2 + (st2095->k / 0.541196) + 1.0); + st2095->lp2_b0 = st2095->k2 / (st2095->k2 + (st2095->k / 0.541196) + 1.0); + st2095->lp2_b1 = 2.0 * st2095->lp2_b0; + st2095->lp2_b2 = st2095->lp2_b0; + + // initialize delay lines for bandpass filter + st2095->hp1w1 = 0.0; + st2095->hp1w2 = 0.0; + st2095->hp2w1 = 0.0; + st2095->hp2w2 = 0.0; + st2095->lp1w1 = 0.0; + st2095->lp1w2 = 0.0; + st2095->lp2w1 = 0.0; + st2095->lp2w2 = 0.0; + + // initialize delay lines for pink filter network + st2095->lp1 = 0.0; + st2095->lp2 = 0.0; + st2095->lp3 = 0.0; + st2095->lp4 = 0.0; + st2095->lp5 = 0.0; + st2095->lp6 = 0.0; + + // cycle the generator for one complete time series to populate filter-bank delay lines + for (int i=0; isamplesPerPeriod; i++) + generate_st2095_noise_sample(st2095); + st2095->accum = 0.0; +} + +float generate_st2095_noise_sample( st2095_noise_t *st2095 ) { + float white, w, pink; + + // Generate a pseudorandom integer in the range 0 <= seed <= randMax. + //# Bitwise AND with randMax zeroes out any unwanted high order bits. + st2095->seed = (1664525 * st2095->seed + st2095->randStep) & st2095->randMax; + // Scale to a real number in the range -1.0 <= white <= 1.0 + white = (float)st2095->seed * st2095->scaleFactor - 1.0; + + // Run pink filter; a parallel network of first-order LP filters, scaled to + // produce an output signal with target RMS = -21.5 dB FS (-18.5 dB AES FS) + // when bandpass filter cutoff frequencies are 10 Hz and 22.4 kHz. + st2095->lp1 = 0.9994551 * st2095->lp1 + 0.00198166688621989 * white; + st2095->lp2 = 0.9969859 * st2095->lp2 + 0.00263702334184061 * white; + st2095->lp3 = 0.9844470 * st2095->lp3 + 0.00643213710202331 * white; + st2095->lp4 = 0.9161757 * st2095->lp4 + 0.01438952538362820 * white; + st2095->lp5 = 0.6563399 * st2095->lp5 + 0.02698408541064610 * white; + pink = st2095->lp1 + st2095->lp2 + st2095->lp3 + + st2095->lp4 + st2095->lp5 + st2095->lp6 + white * 0.0342675832159306; + st2095->lp6 = white * 0.0088766118009356; + + // Run bandpass filter; a series network of 4 biquad filters + // Biquad filters implemented in Direct Form II + w = pink - st2095->hp1_a1 * st2095->hp1w1 - st2095->hp1_a2 * st2095->hp1w2; + pink = st2095->hp1_b0 * w + st2095->hp1_b1 * st2095->hp1w1 + st2095->hp1_b2 * st2095->hp1w2; + st2095->hp1w2 = st2095->hp1w1; + st2095->hp1w1 = w; + + w = pink - st2095->hp2_a1 * st2095->hp2w1 - st2095->hp2_a2 * st2095->hp2w2; + pink = st2095->hp2_b0 * w + st2095->hp2_b1 * st2095->hp2w1 + st2095->hp2_b2 * st2095->hp2w2; + st2095->hp2w2 = st2095->hp2w1; + st2095->hp2w1 = w; + + w = pink - st2095->lp1_a1 * st2095->lp1w1 - st2095->lp1_a2 * st2095->lp1w2; + pink = st2095->lp1_b0 * w + st2095->lp1_b1 * st2095->lp1w1 + st2095->lp1_b2 * st2095->lp1w2; + st2095->lp1w2 = st2095->lp1w1; + st2095->lp1w1 = w; + + w = pink - st2095->lp2_a1 * st2095->lp2w1 - st2095->lp2_a2 * st2095->lp2w2; + pink = st2095->lp2_b0 * w + st2095->lp2_b1 * st2095->lp2w1 + st2095->lp2_b2 * st2095->lp2w2; + st2095->lp2w2 = st2095->lp2w1; + st2095->lp2w1 = w; + + // Limit peaks to +/-MaxAmp + if (pink > st2095->maxAmp) + pink = st2095->maxAmp; + else if (pink < -st2095->maxAmp) + pink = -st2095->maxAmp; + + // accumulate squared amplitude for RMS computation + st2095->accum += (pink * pink); + return(pink); +} diff --git a/speaker-test/st2095.h b/speaker-test/st2095.h new file mode 100644 index 0000000..32083da --- /dev/null +++ b/speaker-test/st2095.h @@ -0,0 +1,41 @@ +#define ST2095_MAX_PEAK -9.5 // dB +#define ST2095_HPFC 10.0 // Highpass filter cutoff in Hz +#define ST2095_LPFC 22400.0 // Lowpass filter cutoff in Hz + +typedef struct +{ + float maxAmp; + int samplesPerPeriod; + int randStep; + int randMax; + int seed; + float scaleFactor; + float w0t; + float k; + float k2; + float LpFc; + // biquad coefficients + float hp1_a1, hp1_a2; + float hp1_b0, hp1_b1, hp1_b2; + float hp2_a1, hp2_a2; + float hp2_b0, hp2_b1, hp2_b2; + float lp1_a1, lp1_a2; + float lp1_b0, lp1_b1, lp1_b2; + float lp2_a1, lp2_a2; + float lp2_b0, lp2_b1, lp2_b2; + // delay-line variables for bandpass filter + float hp1w1, hp1w2; + float hp2w1, hp2w2; + float lp1w1, lp1w2; + float lp2w1, lp2w2; + // delay-line variables for pink filter network + float lp1, lp2, lp3, lp4, lp5, lp6; + // statistics accumulator + float accum; +} st2095_noise_t; + +void initialize_st2095_noise( st2095_noise_t *st2095, int sample_rate ); +float generate_st2095_noise_sample( st2095_noise_t *st2095 ); + +void reset_st2095_noise_measurement( st2095_noise_t *st2095 ); +float compute_st2095_noise_measurement( st2095_noise_t *st2095, int period ); diff --git a/topology/Makefile.am b/topology/Makefile.am new file mode 100644 index 0000000..61055d8 --- /dev/null +++ b/topology/Makefile.am @@ -0,0 +1,25 @@ +SUBDIRS = nhlt + +bin_PROGRAMS = \ + alsatplg + +if USE_RST2MAN +man_MANS = alsatplg.1 +endif + +%.1: %.rst + rst2man $< > $@ + +alsatplg_SOURCES = topology.c pre-processor.c pre-process-class.c pre-process-object.c \ + pre-process-dapm.c pre-process-dai.c + +noinst_HEADERS = topology.h pre-processor.h pre-process-external.h + +AM_CPPFLAGS = \ + -Wall -I$(top_srcdir)/include -DALSA_TOPOLOGY_PLUGIN_DIR=\"@ALSA_TOPOLOGY_PLUGIN_DIR@\" + +alsatplg_LDADD = $(ALSA_TOPOLOGY_LIBS) + +EXTRA_DIST = alsatplg.rst + +CLEANFILES = alsatplg.1 diff --git a/topology/alsatplg.rst b/topology/alsatplg.rst new file mode 100644 index 0000000..56324d5 --- /dev/null +++ b/topology/alsatplg.rst @@ -0,0 +1,102 @@ +========== + alsatplg +========== + +---------------------- +ALSA Topology Compiler +---------------------- + +:Author: Jaroslav Kysela +:Date: 2018-10-09 +:Copyright: GPLv2+ +:Manual section: 1 +:Manual group: General Commands Manual + +SYNOPSIS +======== + +*alsatplg* [command] + +DESCRIPTION +=========== + +alsatplg (ALSA Topology compiler) is a program to compile topology +configuration file to the binary file for the kernel drivers. + +Current audio drivers typically hard code topology information +in the driver sources: This tightly couples the audio driver +to the development board making it time consuming to modify +a driver to work on a different devices. The driver is also +tightly coupled to the DSP firmware version meaning extra care +is needed to keep the driver and firmware version in sync. +New firmware features also mean driver updates. + +The ALSA topology project removes the need for re-writing or +porting audio drivers to different devices or different firmwares: +Drivers have no hard coded topology data meaning a single driver +can be used on different devices by updating the topology data +from the file system. Firmware updates can be pushed without +having to update the drivers. The new firmware just needs +to include an updated topology file describing the update. + +OPTIONS +======= + +Available options: + + **-h**, **--help** + this help + + **-V**, **--version** + show the utility version and versions of used libraries + + **-c**, **--compile** `FILE` + source configuration file for the compilation + + **-d**, **--decode** `FILE` + source binary topology file for the decode + + **-n**, **--normalize** `FILE` + parse and save the configuration file in the normalized format + + **-u**, **--dump** `FILE` + parse and save the configuration file in the specified format + + **-o**, **--output** `FILE` + output file + + **-v**, **--verbose** `LEVEL` + set verbose level + + **-s**, **--sort** + sort the configuration identifiers (set for normalization) + + **-x**, **--nocheck** + save the configuration without additional integrity check + + **-z**, **--dapm-nosort** + do not sort DAPM graph items (like in version 1.2.1-) + + +FILES +===== + +The master topology files for each supported sound card are in +``/usr/share/alsa/topology``. + +For example, the master use case file for the `broadwell` card is in +``/usr/share/alsa/topology/broadwell/broadwell.conf``, this file +describes the audio hardware for the driver. + +For more details on the syntax of UCM files, see the alsa-lib source code: +http://git.alsa-project.org/?p=alsa-lib.git;a=blob;f=src/topology/parser.c + +SEE ALSO +======== + +* Topology Interface: http://www.alsa-project.org/alsa-doc/alsa-lib/group__topology.html + +BUGS +==== + +None known. diff --git a/topology/nhlt/Makefile.am b/topology/nhlt/Makefile.am new file mode 100644 index 0000000..1b68382 --- /dev/null +++ b/topology/nhlt/Makefile.am @@ -0,0 +1,29 @@ +alsatplg_module_nhlt_LTLIBRARIES = libalsatplg_module_nhlt.la + +alsatplg_module_nhltdir = @ALSA_TOPOLOGY_PLUGIN_DIR@ + +AM_CFLAGS = -Wall -fvisibility=hidden -I$(top_srcdir)/include -I$(top_srcdir)/topology +AM_LDFLAGS = -module -avoid-version -export-dynamic -no-undefined $(LDFLAGS_NOUNDEFINED) + +libalsatplg_module_nhlt_la_SOURCES = nhlt-processor.c \ + intel/intel-nhlt.c \ + intel/dmic-nhlt.c \ + intel/dmic/dmic-debug.c intel/dmic/dmic-process.c \ + intel/ssp-nhlt.c \ + intel/ssp/ssp-debug.c intel/ssp/ssp-process.c + +noinst_HEADERS = nhlt.h \ + intel/intel-nhlt.h \ + intel/dmic-nhlt.h \ + intel/dmic/dmic-intel.h \ + intel/dmic/dmic-process.h \ + intel/dmic/dmic-debug.h \ + intel/dmic/dmic-internal.h \ + intel/dmic/pdm-decim-fir.h \ + intel/ssp-nhlt.h \ + intel/ssp/ssp-debug.h \ + intel/ssp/ssp-intel.h \ + intel/ssp/ssp-process.h \ + intel/ssp/ssp-internal.h + +libalsatplg_module_nhlt_la_LIBADD = @ALSA_LIBS@ diff --git a/topology/nhlt/intel/dmic-nhlt.c b/topology/nhlt/intel/dmic-nhlt.c new file mode 100644 index 0000000..7c98a3b --- /dev/null +++ b/topology/nhlt/intel/dmic-nhlt.c @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include "dmic-nhlt.h" +#include "dmic/dmic-process.h" + +static int set_dmic_data(struct intel_nhlt_params *nhlt, snd_config_t *dai_cfg, snd_config_t *top) +{ + long unmute_ramp_time_ms = 0; + long fifo_word_length = 0; + long driver_version = 0; + long num_pdm_active = 0; + long sample_rate = 0; + long dai_index_t = 0; + long duty_min = 0; + long duty_max = 0; + long clk_min = 0; + long clk_max = 0; + long io_clk = 0; + int ret; + + struct dai_values dmic_data[] = { + { "driver_version", SND_CONFIG_TYPE_INTEGER, NULL, &driver_version, NULL}, + { "io_clk", SND_CONFIG_TYPE_INTEGER, NULL, &io_clk, NULL}, + { "dai_index", SND_CONFIG_TYPE_INTEGER, NULL, &dai_index_t, NULL}, + { "num_pdm_active", SND_CONFIG_TYPE_INTEGER, NULL, &num_pdm_active, NULL}, + { "fifo_word_length", SND_CONFIG_TYPE_INTEGER, NULL, &fifo_word_length, NULL}, + { "clk_min", SND_CONFIG_TYPE_INTEGER, NULL, &clk_min, NULL}, + { "clk_max", SND_CONFIG_TYPE_INTEGER, NULL, &clk_max, NULL}, + { "duty_min", SND_CONFIG_TYPE_INTEGER, NULL, &duty_min, NULL}, + { "duty_max", SND_CONFIG_TYPE_INTEGER, NULL, &duty_max, NULL}, + { "sample_rate", SND_CONFIG_TYPE_INTEGER, NULL, &sample_rate, NULL}, + { "unmute_ramp_time_ms", SND_CONFIG_TYPE_INTEGER, NULL, &unmute_ramp_time_ms, NULL}, + }; + + ret = find_set_values(&dmic_data[0], ARRAY_SIZE(dmic_data), dai_cfg, top, "Class.Dai.DMIC"); + if (ret < 0) + return ret; + + return dmic_set_params(nhlt, dai_index_t, driver_version, io_clk, num_pdm_active, + fifo_word_length, clk_min, clk_max, duty_min, duty_max, sample_rate, + unmute_ramp_time_ms); +} + +static int set_pdm_data(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long mic_a_enable = 0; + long mic_b_enable = 0; + long polarity_a = 0; + long polarity_b = 0; + long clk_edge = 0; + long ctrl_id = 0; + long skew = 0; + int ret; + + struct dai_values dmic_pdm_data[] = { + { "mic_a_enable", SND_CONFIG_TYPE_INTEGER, NULL, &mic_a_enable, NULL}, + { "mic_b_enable", SND_CONFIG_TYPE_INTEGER, NULL, &mic_b_enable, NULL}, + { "polarity_a", SND_CONFIG_TYPE_INTEGER, NULL, &polarity_a, NULL}, + { "polarity_b", SND_CONFIG_TYPE_INTEGER, NULL, &polarity_b, NULL}, + { "clk_edge", SND_CONFIG_TYPE_INTEGER, NULL, &clk_edge, NULL}, + { "ctrl_id", SND_CONFIG_TYPE_INTEGER, NULL, &ctrl_id, NULL}, + { "skew", SND_CONFIG_TYPE_INTEGER, NULL, &skew, NULL}, + }; + + ret = find_set_values(&dmic_pdm_data[0], ARRAY_SIZE(dmic_pdm_data), cfg, top, + "Class.Base.pdm_config"); + if (ret < 0) + return ret; + + return dmic_set_pdm_params(nhlt, ctrl_id, mic_a_enable, mic_b_enable, polarity_a, + polarity_b, clk_edge, skew); +} + +static int set_mic_data(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long sensitivity = 0; + long snr = 0; + int ret; + + struct dai_values dmic_mic_data[] = { + { "snr", SND_CONFIG_TYPE_INTEGER, NULL, &snr, NULL}, + { "sensitivity", SND_CONFIG_TYPE_INTEGER, NULL, &snr, NULL}, + }; + + ret = find_set_values(&dmic_mic_data[0], ARRAY_SIZE(dmic_mic_data), cfg, top, + "Class.Base.mic_extension"); + if (ret < 0) + return ret; + + return dmic_set_ext_params(nhlt, snr, sensitivity); +} + +static int set_vendor_mic_data(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long speaker_position_distance = 0; + long horizontal_angle_begin = 0; + long horizontal_angle_end = 0; + long vertical_angle_begin = 0; + long vertical_angle_end = 0; + long frequency_high_band = 0; + long frequency_low_band = 0; + long horizontal_offset = 0; + long vertical_offset = 0; + long direction_angle = 0; + long elevation_angle = 0; + long mic_type = 0; + long location = 0; + long mic_id = 0; + int ret; + + struct dai_values dmic_vendor_data[] = { + { "mic_id", SND_CONFIG_TYPE_INTEGER, NULL, &mic_id, NULL}, + { "mic_type", SND_CONFIG_TYPE_INTEGER, NULL, &mic_type, NULL}, + { "location", SND_CONFIG_TYPE_INTEGER, NULL, &location, NULL}, + { "speaker_position_distance", SND_CONFIG_TYPE_INTEGER, NULL, + &speaker_position_distance, NULL}, + { "horizontal_offset", SND_CONFIG_TYPE_INTEGER, NULL, &horizontal_offset, NULL}, + { "vertical_offset", SND_CONFIG_TYPE_INTEGER, NULL, &vertical_offset, NULL}, + { "frequency_low_band", SND_CONFIG_TYPE_INTEGER, NULL, &frequency_low_band, NULL}, + { "frequency_high_band", SND_CONFIG_TYPE_INTEGER, NULL, &frequency_high_band, NULL}, + { "direction_angle", SND_CONFIG_TYPE_INTEGER, NULL, &direction_angle, NULL}, + { "elevation_angle", SND_CONFIG_TYPE_INTEGER, NULL, &elevation_angle, NULL}, + { "vertical_angle_begin", SND_CONFIG_TYPE_INTEGER, NULL, &vertical_angle_begin, + NULL}, + { "vertical_angle_end", SND_CONFIG_TYPE_INTEGER, NULL, &vertical_angle_end, NULL}, + { "horizontal_angle_begin", SND_CONFIG_TYPE_INTEGER, NULL, &horizontal_angle_begin, + NULL}, + { "horizontal_angle_end", SND_CONFIG_TYPE_INTEGER, NULL, &horizontal_angle_end, + NULL}, + }; + + ret = find_set_values(&dmic_vendor_data[0], ARRAY_SIZE(dmic_vendor_data), cfg, top, + "Class.Base.vendor_mic_config"); + if (ret < 0) + return ret; + + return dmic_set_mic_params(nhlt, mic_id, mic_type, location, speaker_position_distance, + horizontal_offset, vertical_offset, frequency_low_band, + frequency_high_band, direction_angle, elevation_angle, + vertical_angle_begin, vertical_angle_end, horizontal_angle_begin, + horizontal_angle_end); +} + +static int set_bytes_data(struct intel_nhlt_params *nhlt ATTRIBUTE_UNUSED, snd_config_t *cfg) +{ + snd_config_iterator_t i, next; + snd_config_t *n; + const char *bytes; + const char *id; + + if (snd_config_get_id(cfg, &id) < 0) + return -EINVAL; + + if (strcmp(id, "fir_coeffs")) + return 0; + + snd_config_for_each(i, next, cfg) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_string(n, &bytes)) + return -EINVAL; + } + + return 0; +} + +/* init dmic parameters, should be called before parsing dais */ +int nhlt_dmic_init_params(struct intel_nhlt_params *nhlt) +{ + return dmic_init_params(nhlt); +} + +/* get dmic endpoint count */ +int nhlt_dmic_get_ep_count(struct intel_nhlt_params *nhlt) +{ + return dmic_get_vendor_blob_count(nhlt); +} + +int nhlt_dmic_get_ep(struct intel_nhlt_params *nhlt, struct endpoint_descriptor **eps, + int index) +{ + struct endpoint_descriptor ep; + struct mic_array_device_specific_config mic_s_conf; + struct mic_array_device_specific_vendor_config mic_v_conf; + struct mic_snr_sensitivity_extension mic_ext; + struct mic_vendor_config mic_conf; + struct formats_config f_conf; + struct format_config f_conf1; + uint8_t *ep_target; + size_t blob_size; + int ret; + int i; + + size_t mic_config_size; + uint32_t sample_rate; + uint16_t channel_count; + uint32_t bits_per_sample; + uint8_t array_type; + uint8_t extension; + uint8_t num_mics; + uint32_t snr; + uint32_t sensitivity; + + uint8_t type; + uint8_t panel; + uint32_t speaker_position_distance; + uint32_t horizontal_offset; + uint32_t vertical_offset; + uint8_t frequency_low_band; + uint8_t frequency_high_band; + uint16_t direction_angle; + uint16_t elevation_angle; + uint16_t vertical_angle_begin; + uint16_t vertical_angle_end; + uint16_t horizontal_angle_begin; + uint16_t horizontal_angle_end; + + /* + * nhlt dmic structure: + * + * endpoint_descriptor, sizeof(struct endpoint_descriptor) + * + * device_specific_config (mic), sizeof(mic_array_device_specific_config) + * or + * device_specific_config (mic), sizeof(mic_array_device_specific_vendor_config) + * + * formats_config (formats_count), sizeof(struct formats_config) + * format_config (waveex), sizeof(struct format_config) + * vendor_blob sizeof(vendor_blob) + */ + + /* dmic ep */ + ep.link_type = NHLT_LINK_TYPE_PDM; + ep.instance_id = 0; + ep.vendor_id = NHLT_VENDOR_ID_INTEL; + ep.device_id = NHLT_DEVICE_ID_INTEL_PDM_DMIC; + ep.revision_id = 0; + ep.subsystem_id = 0; + ep.device_type = 0; + ep.direction = NHLT_ENDPOINT_DIRECTION_CAPTURE; + ep.virtualbus_id = index; + + ret = dmic_get_params(nhlt, index, &sample_rate, &channel_count, &bits_per_sample, + &array_type, &num_mics, &extension, &snr, &sensitivity); + + if (ret) { + fprintf(stderr, "nhlt_dmic_get_ep: dmic_get_params failed\n"); + return ret; + } + + if (array_type == NHLT_MIC_ARRAY_TYPE_VENDOR_DEFINED) { + mic_v_conf.config.capabilities_size = 4 + num_mics * + sizeof(struct mic_vendor_config); + mic_v_conf.device_config.virtual_slot = 0; /* always 0 for dmic */ + mic_v_conf.device_config.config_type = NHLT_DEVICE_CONFIG_TYPE_MICARRAY; + mic_v_conf.number_of_microphones = num_mics; + mic_v_conf.array_type_ex = array_type; + /* precense of extension struct is coded into lower 4 bits of array_type */ + if (extension) { + mic_v_conf.array_type_ex = (array_type & ~0x0F) | (0x01 & 0x0F); + mic_v_conf.config.capabilities_size += + sizeof(struct mic_snr_sensitivity_extension); + } + } else { + mic_s_conf.config.capabilities_size = 3; + mic_s_conf.device_config.virtual_slot = 0; /* always 0 for dmic */ + mic_s_conf.device_config.config_type = NHLT_DEVICE_CONFIG_TYPE_MICARRAY; + mic_s_conf.array_type_ex = array_type; + /* presense of extension struct coded into lower 4 bits of array_type */ + if (extension) { + mic_s_conf.array_type_ex = (array_type & ~0x0F) | (0x01 & 0x0F); + mic_s_conf.config.capabilities_size += + sizeof(struct mic_snr_sensitivity_extension); + } + } + + /* formats_config */ + f_conf.formats_count = 1; + + /* fill in wave format extensible types */ + f_conf1.format.wFormatTag = 0xFFFE; + f_conf1.format.nSamplesPerSec = sample_rate; + f_conf1.format.nChannels = channel_count; + f_conf1.format.wBitsPerSample = bits_per_sample; + f_conf1.format.nBlockAlign = channel_count * bits_per_sample / 8; + f_conf1.format.nAvgBytesPerSec = f_conf1.format.nSamplesPerSec * f_conf1.format.nBlockAlign; + + /* bytes after this value in this struct */ + f_conf1.format.cbSize = 22; + /* actual bits in container */ + f_conf1.format.wValidBitsPerSample = bits_per_sample; + /* channel map not used at this time */ + f_conf1.format.dwChannelMask = 0; + /* WAVE_FORMAT_PCM guid (0x0001) ? */ + f_conf1.format.SubFormat[0] = 0; + f_conf1.format.SubFormat[1] = 0; + f_conf1.format.SubFormat[2] = 0; + f_conf1.format.SubFormat[3] = 0; + + ret = dmic_get_vendor_blob_size(nhlt, &blob_size); + if (ret) { + fprintf(stderr, "nhlt_dmic_get_ep: dmic_get_vendor_blob_size failed\n"); + return ret; + } + + f_conf1.vendor_blob.capabilities_size = blob_size; + + if (array_type == NHLT_MIC_ARRAY_TYPE_VENDOR_DEFINED) + mic_config_size = sizeof(struct mic_array_device_specific_vendor_config) + + num_mics * sizeof(struct mic_vendor_config); + else + mic_config_size = sizeof(struct mic_array_device_specific_config); + + if (extension) + mic_config_size = sizeof(struct mic_snr_sensitivity_extension); + + ep.length = sizeof(struct endpoint_descriptor) + + mic_config_size + + sizeof(struct formats_config) + + sizeof(struct format_config) + + blob_size; + + /* allocate the final variable length ep struct */ + ep_target = calloc(ep.length, sizeof(uint8_t)); + if (!ep_target) + return -ENOMEM; + + *eps = (struct endpoint_descriptor *)ep_target; + + /* copy all parsed sub arrays into the top level array */ + memcpy(ep_target, &ep, sizeof(struct endpoint_descriptor)); + + ep_target += sizeof(struct endpoint_descriptor); + + if (array_type == NHLT_MIC_ARRAY_TYPE_VENDOR_DEFINED) { + memcpy(ep_target, &mic_v_conf, + sizeof(struct mic_array_device_specific_vendor_config)); + ep_target += sizeof(struct mic_array_device_specific_vendor_config); + for (i = 0; i < num_mics; i++) { + ret = dmic_get_mic_params(nhlt, i, &type, + &panel, &speaker_position_distance, + &horizontal_offset, &vertical_offset, + &frequency_low_band, &frequency_high_band, + &direction_angle, &elevation_angle, + &vertical_angle_begin, &vertical_angle_end, + &horizontal_angle_begin, &horizontal_angle_end); + + if (ret) { + fprintf(stderr, "nhlt_dmic_get_ep: dmic_get_mic_params failed\n"); + return ret; + } + + mic_conf.type = type; + mic_conf.panel = panel; + mic_conf.speaker_position_distance = speaker_position_distance; + mic_conf.horizontal_offset = horizontal_offset; + mic_conf.vertical_offset = vertical_offset; + mic_conf.frequency_low_band = frequency_low_band; + mic_conf.frequency_high_band = frequency_high_band; + mic_conf.direction_angle = direction_angle; + mic_conf.elevation_angle = elevation_angle; + mic_conf.vertical_angle_begin = vertical_angle_begin; + mic_conf.vertical_angle_end = vertical_angle_end; + mic_conf.horizontal_angle_begin = horizontal_angle_begin; + mic_conf.horizontal_angle_end = horizontal_angle_end; + + memcpy(ep_target, &mic_conf, sizeof(struct mic_vendor_config)); + ep_target += sizeof(struct mic_vendor_config); + } + } else { + memcpy(ep_target, &mic_s_conf, sizeof(struct mic_array_device_specific_config)); + ep_target += sizeof(struct mic_array_device_specific_config); + } + + if (extension) { + mic_ext.snr = snr; + mic_ext.sensitivity = sensitivity; + memcpy(ep_target, &mic_ext, sizeof(struct mic_snr_sensitivity_extension)); + ep_target += sizeof(struct mic_snr_sensitivity_extension); + } + + memcpy(ep_target, &f_conf, sizeof(struct formats_config)); + ep_target += sizeof(struct formats_config); + + memcpy(ep_target, &f_conf1, sizeof(struct format_config)); + ep_target += sizeof(struct format_config); + + ret = dmic_get_vendor_blob(nhlt, ep_target); + if (ret) { + fprintf(stderr, "nhlt_dmic_get_ep: dmic_get_vendor_blob failed\n"); + return ret; + } + + return 0; +} + +/* + * Set dmic parameters from topology for dmic coefficient calculation. + * + * Coefficients are recalculated in case of multiple DAIs in topology and might affect each other. + * + * You can see an example of topology v2 config of dmic below. In this example the default + * object parameters are spelled out for clarity. General parameters like clk_min are parsed with + * set_dmic_data and pdm object data with set_pdm_data. Number of pdm's can vary from 1 to 2. Values + * are saved into intermediate structs and the vendor specific blob is calculated at the end of + * parsing with dmic_calculate. + * + * DMIC."0" { + * name NoCodec-6 + * id 6 + * index 0 + * driver_version 1 + * io_clk 38400000 + * clk_min 500000 + * clk_max 4800000 + * duty_min 40 + * duty_max 60 + * sample_rate 48000 + * fifo_word_length 16 + * unmute_ramp_time_ms 200 + * num_pdm_active 2 + * + * # PDM controller config + * Object.Base.pdm_config."0" { + * ctrl_id 0 + * mic_a_enable 1 + * mic_b_enable 1 + * polarity_a 0 + * polarity_b 0 + * clk_edge 0 + * skew 0 + * } + * } + */ +int nhlt_dmic_set_params(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + snd_config_t *items; + int ret; + snd_config_iterator_t i, next; + snd_config_t *n; + const char *id; + + /* set basic dmic data */ + ret = set_dmic_data(nhlt, cfg, top); + if (ret < 0) + return ret; + + /* we need to have at least one pdm object */ + ret = snd_config_search(cfg, "Object.Base.pdm_config", &items); + if (ret < 0) + return ret; + + snd_config_for_each(i, next, items) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + ret = set_pdm_data(nhlt, n, top); + if (ret < 0) + return ret; + } + + /* check for microphone parameter configuration */ + ret = snd_config_search(cfg, "Object.Base.mic_extension", &items); + if (!ret) { + snd_config_for_each(i, next, items) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + ret = set_mic_data(nhlt, n, top); + if (ret < 0) + return ret; + } + } + + /* check for microphone parameter configuration */ + ret = snd_config_search(cfg, "Object.Base.vendor_mic_config", &items); + if (!ret) { + snd_config_for_each(i, next, items) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + set_vendor_mic_data(nhlt, n, top); + } + } + + /* check for optional filter coeffs */ + ret = snd_config_search(cfg, "Object.Base.data", &items); + if (!ret) { + snd_config_for_each(i, next, items) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + set_bytes_data(nhlt, n); + } + } + + ret = dmic_calculate(nhlt); + + return ret; +} diff --git a/topology/nhlt/intel/dmic-nhlt.h b/topology/nhlt/intel/dmic-nhlt.h new file mode 100644 index 0000000..73aaad5 --- /dev/null +++ b/topology/nhlt/intel/dmic-nhlt.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#ifndef __DMIC_NHLT_H +#define __DMIC_NHLT_H + +#include "intel-nhlt.h" +#include "../nhlt.h" + +int nhlt_dmic_init_params(struct intel_nhlt_params *nhlt); +int nhlt_dmic_set_params(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top); +int nhlt_dmic_get_ep(struct intel_nhlt_params *nhlt, struct endpoint_descriptor **eps, + int index); +int nhlt_dmic_get_ep_count(struct intel_nhlt_params *nhlt); + +#endif diff --git a/topology/nhlt/intel/dmic/dmic-debug.c b/topology/nhlt/intel/dmic/dmic-debug.c new file mode 100644 index 0000000..82c6113 --- /dev/null +++ b/topology/nhlt/intel/dmic/dmic-debug.c @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include "dmic-debug.h" + +#ifdef NHLT_DEBUG + +/* print blob as bytes hex string like: 0x11,0xff,0xff,0xff etc. */ +void dmic_print_bytes_as_hex(uint8_t *src, size_t size) +{ + int i, j, lines, remain; + + fprintf(stdout, "printing dmic vendor blob as bytes:\n"); + + lines = size / 8; + remain = size % 8; + for (i = 0; i < lines; i++) { + for (j = 0; j < 8; j++) { + fprintf(stdout, "0x%02x,", *src); + src++; + } + fprintf(stdout, "\n"); + } + for (i = 0; i < remain; i++) { + fprintf(stdout, "0x%02x,", *src); + src++; + } + + fprintf(stdout, "\n"); +} + +/* print blob as 32 bit integer hex string like: 0xffffffff,0x00000010 etc. */ +void dmic_print_integers_as_hex(uint32_t *src, size_t size) +{ + int i, j, lines, remain; + + fprintf(stdout, "printing dmic vendor blob as integers:\n"); + + lines = size / 8; + remain = size % 8; + for (i = 0; i < lines; i++) { + for (j = 0; j < 8; j++) { + fprintf(stdout, "0x%08x,", *src); + src++; + } + fprintf(stdout, "\n"); + } + for (i = 0; i < remain; i++) { + fprintf(stdout, "0x%08x,", *src); + src++; + } + + fprintf(stdout, "\n"); +} + +void dmic_print_internal(struct intel_dmic_params *dmic) +{ + int i, j, line, lines, remain; + + fprintf(stdout, "printing dmic nhlt internal data:\n"); + + /* top level struct */ + fprintf(stdout, "gateway attributes: 0x%08x\n", dmic->dmic_blob.gateway_attributes); + + fprintf(stdout, "ts_group: 0x%08x 0x%08x 0x%08x 0x%08x\n", dmic->dmic_blob.ts_group[0], + dmic->dmic_blob.ts_group[1], dmic->dmic_blob.ts_group[2], + dmic->dmic_blob.ts_group[3]); + + fprintf(stdout, "clock_on_delay: 0x%08x\n", dmic->dmic_blob.clock_on_delay); + + fprintf(stdout, "channel_ctrl_mask: 0x%08x\n", dmic->dmic_blob.channel_ctrl_mask); + + fprintf(stdout, "chan_ctrl_cfg: 0x%08x 0x%08x\n", dmic->dmic_blob.chan_ctrl_cfg[0], + dmic->dmic_blob.chan_ctrl_cfg[1]); + + fprintf(stdout, "channel_pdm_mask: 0x%08x\n", dmic->dmic_blob.channel_pdm_mask); + + /* first pdm struct */ + fprintf(stdout, "pdm_ctrl_cfg 0\n"); + fprintf(stdout, "cic_control: 0x%08x\n", dmic->dmic_blob_pdm[0].cic_control); + fprintf(stdout, "cic_config: 0x%08x\n", dmic->dmic_blob_pdm[0].cic_config); + fprintf(stdout, "mic_control: 0x%08x\n", dmic->dmic_blob_pdm[0].mic_control); + fprintf(stdout, "pdmsm: 0x%08x\n", dmic->dmic_blob_pdm[0].pdmsm); + fprintf(stdout, "reuse_fir_from_pdm: 0x%08x\n", dmic->dmic_blob_pdm[0].reuse_fir_from_pdm); + + /* first pdm struct, first fir */ + fprintf(stdout, "fir_config 0\n"); + fprintf(stdout, "fir_control: 0x%08x\n", dmic->dmic_blob_fir[0][0].fir_control); + fprintf(stdout, "fir_config: 0x%08x\n", dmic->dmic_blob_fir[0][0].fir_config); + fprintf(stdout, "dc_offset_left: 0x%08x\n", dmic->dmic_blob_fir[0][0].dc_offset_left); + fprintf(stdout, "dc_offset_right: 0x%08x\n", dmic->dmic_blob_fir[0][0].dc_offset_right); + fprintf(stdout, "out_gain_left: 0x%08x\n", dmic->dmic_blob_fir[0][0].out_gain_left); + fprintf(stdout, "out_gain_right: 0x%08x\n", dmic->dmic_blob_fir[0][0].out_gain_right); + + /* first pdm struct, second fir */ + fprintf(stdout, "fir_config 1\n"); + fprintf(stdout, "fir_control: 0x%08x\n", dmic->dmic_blob_fir[0][1].fir_control); + fprintf(stdout, "fir_config: 0x%08x\n", dmic->dmic_blob_fir[0][1].fir_config); + fprintf(stdout, "dc_offset_left: 0x%08x\n", dmic->dmic_blob_fir[0][1].dc_offset_left); + fprintf(stdout, "dc_offset_right: 0x%08x\n", dmic->dmic_blob_fir[0][1].dc_offset_right); + fprintf(stdout, "out_gain_left: 0x%08x\n", dmic->dmic_blob_fir[0][1].out_gain_left); + fprintf(stdout, "out_gain_right: 0x%08x\n", dmic->dmic_blob_fir[0][1].out_gain_right); + + /* first pdm struct, fir coeffs */ + for (j = 0; j < DMIC_HW_CONTROLLERS; j++) { + fprintf(stdout, "fir_coeffs a length %u:\n", dmic->dmic_fir_array.fir_len[0]); + lines = dmic->dmic_fir_array.fir_len[0] / 8; + remain = dmic->dmic_fir_array.fir_len[0] % 8; + for (i = 0; i < lines; i++) { + line = i * 8; + fprintf(stdout, "%d %d %d %d %d %d %d %d %d\n", i, + dmic->dmic_fir_array.fir_coeffs[j][0][line], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 1], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 2], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 3], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 4], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 5], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 6], + dmic->dmic_fir_array.fir_coeffs[j][0][line + 7]); + } + line += 1; + for (i = 0; i < remain; i++) + fprintf(stdout, "%d ", dmic->dmic_fir_array.fir_coeffs[j][0][line + i]); + } + + /* second pdm struct */ + fprintf(stdout, "pdm_ctrl_cfg 1\n"); + fprintf(stdout, "cic_control: 0x%08x\n", dmic->dmic_blob_pdm[1].cic_control); + fprintf(stdout, "cic_config: 0x%08x\n", dmic->dmic_blob_pdm[1].cic_config); + fprintf(stdout, "mic_control: 0x%08x\n", dmic->dmic_blob_pdm[1].mic_control); + fprintf(stdout, "pdmsm: 0x%08x\n", dmic->dmic_blob_pdm[1].pdmsm); + fprintf(stdout, "reuse_fir_from_pdm: 0x%08x\n", dmic->dmic_blob_pdm[1].reuse_fir_from_pdm); + + /* second pdm struct, first fir */ + fprintf(stdout, "fir_config 0\n"); + fprintf(stdout, "fir_control: 0x%08x\n", dmic->dmic_blob_fir[1][0].fir_control); + fprintf(stdout, "fir_config: 0x%08x\n", dmic->dmic_blob_fir[1][0].fir_config); + fprintf(stdout, "dc_offset_left: 0x%08x\n", dmic->dmic_blob_fir[1][0].dc_offset_left); + fprintf(stdout, "dc_offset_right: 0x%08x\n", dmic->dmic_blob_fir[1][0].dc_offset_right); + fprintf(stdout, "out_gain_left: 0x%08x\n", dmic->dmic_blob_fir[1][0].out_gain_left); + fprintf(stdout, "out_gain_right: 0x%08x\n", dmic->dmic_blob_fir[1][0].out_gain_right); + + /* second pdm struct, second fir */ + fprintf(stdout, "fir_config 1\n"); + fprintf(stdout, "fir_control: 0x%08x\n", dmic->dmic_blob_fir[1][1].fir_control); + fprintf(stdout, "fir_config: 0x%08x\n", dmic->dmic_blob_fir[1][1].fir_config); + fprintf(stdout, "dc_offset_left: 0x%08x\n", dmic->dmic_blob_fir[1][1].dc_offset_left); + fprintf(stdout, "dc_offset_right: 0x%08x\n", dmic->dmic_blob_fir[1][1].dc_offset_right); + fprintf(stdout, "out_gain_left: 0x%08x\n", dmic->dmic_blob_fir[1][1].out_gain_left); + fprintf(stdout, "out_gain_right: 0x%08x\n", dmic->dmic_blob_fir[1][1].out_gain_right); + + for (j = 0; j < DMIC_HW_CONTROLLERS; j++) { + fprintf(stdout, "fir_coeffs b length %u:\n", dmic->dmic_fir_array.fir_len[1]); + lines = dmic->dmic_fir_array.fir_len[1] / 8; + remain = dmic->dmic_fir_array.fir_len[1] % 8; + for (i = 0; i < lines; i++) { + line = i * 8; + fprintf(stdout, "%d %d %d %d %d %d %d %d %d\n", i, + dmic->dmic_fir_array.fir_coeffs[j][1][line], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 1], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 2], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 3], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 4], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 5], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 6], + dmic->dmic_fir_array.fir_coeffs[j][1][line + 7]); + } + line += 1; + for (i = 0; i < remain; i++) + fprintf(stdout, "%d ", dmic->dmic_fir_array.fir_coeffs[j][1][line + i]); + } + + fprintf(stdout, "\n"); +} + +#else /* NHLT_DEBUG */ +void dmic_print_bytes_as_hex(uint8_t *src ATTRIBUTE_UNUSED, size_t size ATTRIBUTE_UNUSED) {} +void dmic_print_integers_as_hex(uint32_t *src ATTRIBUTE_UNUSED, size_t size ATTRIBUTE_UNUSED) {} +void dmic_print_internal(struct intel_dmic_params *dmic ATTRIBUTE_UNUSED) {} +#endif diff --git a/topology/nhlt/intel/dmic/dmic-debug.h b/topology/nhlt/intel/dmic/dmic-debug.h new file mode 100644 index 0000000..858b65d --- /dev/null +++ b/topology/nhlt/intel/dmic/dmic-debug.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#ifndef __DMIC_DEBUG_H +#define __DMIC_DEBUG_H + +#include "dmic-internal.h" + +void dmic_print_bytes_as_hex(uint8_t *src, size_t size); +void dmic_print_integers_as_hex(uint32_t *src, size_t size); +void dmic_print_internal(struct intel_dmic_params *dmic); + +#endif diff --git a/topology/nhlt/intel/dmic/dmic-intel.h b/topology/nhlt/intel/dmic/dmic-intel.h new file mode 100644 index 0000000..253aca9 --- /dev/null +++ b/topology/nhlt/intel/dmic/dmic-intel.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#ifndef __DMIC_INTEL_H +#define __DMIC_INTEL_H + +#include + +#define DMIC_TS_GROUP_SIZE 4 + +/* structs for intel dmic nhlt vendor specific blob generation */ +struct dmic_intel_fir_config { + uint32_t fir_control; + uint32_t fir_config; + uint32_t dc_offset_left; + uint32_t dc_offset_right; + uint32_t out_gain_left; + uint32_t out_gain_right; + uint32_t reserved2[2]; + uint32_t fir_coeffs[]; +} __attribute__((packed)); + +struct dmic_intel_pdm_ctrl_cfg { + uint32_t cic_control; + uint32_t cic_config; + uint32_t reserved0; + uint32_t mic_control; + uint32_t pdmsm; + uint32_t reuse_fir_from_pdm; + uint32_t reserved1[2]; + struct dmic_intel_fir_config fir_config[]; +} __attribute__((packed)); + +struct dmic_intel_config_data { + uint32_t gateway_attributes; + uint32_t ts_group[DMIC_TS_GROUP_SIZE]; + uint32_t clock_on_delay; + uint32_t channel_ctrl_mask; + uint32_t chan_ctrl_cfg[2]; + uint32_t channel_pdm_mask; + struct dmic_intel_pdm_ctrl_cfg pdm_ctrl_cfg[]; +} __attribute__((packed)); + +#endif /* __DMIC_INTEL_H */ diff --git a/topology/nhlt/intel/dmic/dmic-internal.h b/topology/nhlt/intel/dmic/dmic-internal.h new file mode 100644 index 0000000..bbdfd34 --- /dev/null +++ b/topology/nhlt/intel/dmic/dmic-internal.h @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#ifndef __DMIC_MACROS_H +#define __DMIC_MACROS_H + +#include "dmic-intel.h" + +#define DMIC_HW_CONTROLLERS 2 +#define DMIC_HW_FIFOS 2 +#define DMIC_HW_FIR_LENGTH_MAX 250 + +/* Get max and min signed integer values for N bits word length */ +#define INT_MAX(N) ((int64_t)((1ULL << ((N) - 1)) - 1)) + +/* Fractional multiplication with shift and round + * Note that the parameters px and py must be cast to (int64_t) if other type. + */ +#define Q_MULTSR_32X32(px, py, qx, qy, qp) \ + ((((px) * (py) >> ((qx) + (qy) - (qp) - 1)) + 1) >> 1) + +/* Convert a float number to fractional Qnx.ny format. Note that there is no + * check for nx+ny number of bits to fit the word length of int. The parameter + * qy must be 31 or less. + */ +#define Q_CONVERT_FLOAT(f, qy) \ + ((int32_t)(((const double)f) * ((int64_t)1 << (const int)qy) + 0.5)) + +/* Saturation */ +#define SATP_INT32(x) (((x) > INT32_MAX) ? INT32_MAX : (x)) + +#define DMIC_MAX_MODES 50 +#define DMIC_FIR_PIPELINE_OVERHEAD 5 + +/* Minimum OSR is always applied for 48 kHz and less sample rates */ +#define DMIC_MIN_OSR 50 + +/* These are used as guideline for configuring > 48 kHz sample rates. The + * minimum OSR can be relaxed down to 40 (use 3.84 MHz clock for 96 kHz). + */ +#define DMIC_HIGH_RATE_MIN_FS 64000 +#define DMIC_HIGH_RATE_OSR_MIN 40 + +/* Used for scaling FIR coefficients for HW */ +#define DMIC_HW_FIR_COEF_MAX ((1 << (DMIC_HW_BITS_FIR_COEF - 1)) - 1) +#define DMIC_HW_FIR_COEF_Q (DMIC_HW_BITS_FIR_COEF - 1) + +/* Internal precision in gains computation, e.g. Q4.28 in int32_t */ +#define DMIC_FIR_SCALE_Q 28 + +/* Parameters used in modes computation */ +#define DMIC_HW_BITS_CIC 26 +#define DMIC_HW_BITS_FIR_COEF 20 +#define DMIC_HW_BITS_FIR_GAIN 20 +#define DMIC_HW_BITS_FIR_INPUT 22 +#define DMIC_HW_BITS_FIR_OUTPUT 24 +#define DMIC_HW_BITS_FIR_INTERNAL 26 +#define DMIC_HW_BITS_GAIN_OUTPUT 22 +#define DMIC_HW_CIC_SHIFT_MIN -8 +#define DMIC_HW_CIC_SHIFT_MAX 4 +#define DMIC_HW_FIR_SHIFT_MIN 0 +#define DMIC_HW_FIR_SHIFT_MAX 8 +#define DMIC_HW_CIC_DECIM_MIN 5 +#define DMIC_HW_CIC_DECIM_MAX 31 /* Note: Limited by BITS_CIC */ +#define DMIC_HW_FIR_DECIM_MIN 2 +#define DMIC_HW_FIR_DECIM_MAX 20 /* Note: Practical upper limit */ +#define DMIC_HW_SENS_Q28 Q_CONVERT_FLOAT(1.0, 28) /* Q1.28 */ +#define DMIC_HW_PDM_CLK_MIN 100000 /* Note: Practical min value */ +#define DMIC_HW_DUTY_MIN 20 /* Note: Practical min value */ +#define DMIC_HW_DUTY_MAX 80 /* Note: Practical max value */ + +/* OUTCONTROL0 bits */ +#define OUTCONTROL0_TIE_BIT BIT(27) +#define OUTCONTROL0_SIP_BIT BIT(26) +#define OUTCONTROL0_FINIT_BIT BIT(25) +#define OUTCONTROL0_FCI_BIT BIT(24) +#define OUTCONTROL0_TIE(x) SET_BIT(27, x) +#define OUTCONTROL0_SIP(x) SET_BIT(26, x) +#define OUTCONTROL0_FINIT(x) SET_BIT(25, x) +#define OUTCONTROL0_FCI(x) SET_BIT(24, x) +#define OUTCONTROL0_BFTH(x) SET_BITS(23, 20, x) +#define OUTCONTROL0_OF(x) SET_BITS(19, 18, x) +#define OUTCONTROL0_TH(x) SET_BITS(5, 0, x) + +/* OUTCONTROL1 bits */ +#define OUTCONTROL1_TIE_BIT BIT(27) +#define OUTCONTROL1_SIP_BIT BIT(26) +#define OUTCONTROL1_FINIT_BIT BIT(25) +#define OUTCONTROL1_FCI_BIT BIT(24) +#define OUTCONTROL1_TIE(x) SET_BIT(27, x) +#define OUTCONTROL1_SIP(x) SET_BIT(26, x) +#define OUTCONTROL1_FINIT(x) SET_BIT(25, x) +#define OUTCONTROL1_FCI(x) SET_BIT(24, x) +#define OUTCONTROL1_BFTH(x) SET_BITS(23, 20, x) +#define OUTCONTROL1_OF(x) SET_BITS(19, 18, x) +#define OUTCONTROL1_TH(x) SET_BITS(5, 0, x) + +/* OUTCONTROL0 bits ver1*/ +#define OUTCONTROL0_IPM_VER1(x) SET_BITS(17, 16, x) +/* OUTCONTROL1 bits ver1 */ +#define OUTCONTROL1_IPM_VER1(x) SET_BITS(17, 16, x) + +/* OUTCONTROL0 bits */ +#define OUTCONTROL0_IPM_VER2(x) SET_BITS(17, 15, x) +#define OUTCONTROL0_IPM_SOURCE_1(x) SET_BITS(14, 13, x) +#define OUTCONTROL0_IPM_SOURCE_2(x) SET_BITS(12, 11, x) +#define OUTCONTROL0_IPM_SOURCE_3(x) SET_BITS(10, 9, x) +#define OUTCONTROL0_IPM_SOURCE_4(x) SET_BITS(8, 7, x) +#define OUTCONTROL0_IPM_SOURCE_MODE(x) SET_BIT(6, x) + +/* OUTCONTROL1 bits */ +#define OUTCONTROL1_IPM_VER2(x) SET_BITS(17, 15, x) +#define OUTCONTROL1_IPM_SOURCE_1(x) SET_BITS(14, 13, x) +#define OUTCONTROL1_IPM_SOURCE_2(x) SET_BITS(12, 11, x) +#define OUTCONTROL1_IPM_SOURCE_3(x) SET_BITS(10, 9, x) +#define OUTCONTROL1_IPM_SOURCE_4(x) SET_BITS(8, 7, x) +#define OUTCONTROL1_IPM_SOURCE_MODE(x) SET_BIT(6, x) + +#define OUTCONTROLX_IPM_NUMSOURCES 4 + +/* CIC_CONTROL bits */ +#define CIC_CONTROL_SOFT_RESET_BIT BIT(16) +#define CIC_CONTROL_CIC_START_B_BIT BIT(15) +#define CIC_CONTROL_CIC_START_A_BIT BIT(14) +#define CIC_CONTROL_MIC_B_POLARITY_BIT BIT(3) +#define CIC_CONTROL_MIC_A_POLARITY_BIT BIT(2) +#define CIC_CONTROL_MIC_MUTE_BIT BIT(1) +#define CIC_CONTROL_STEREO_MODE_BIT BIT(0) + +#define CIC_CONTROL_SOFT_RESET(x) SET_BIT(16, x) +#define CIC_CONTROL_CIC_START_B(x) SET_BIT(15, x) +#define CIC_CONTROL_CIC_START_A(x) SET_BIT(14, x) +#define CIC_CONTROL_MIC_B_POLARITY(x) SET_BIT(3, x) +#define CIC_CONTROL_MIC_A_POLARITY(x) SET_BIT(2, x) +#define CIC_CONTROL_MIC_MUTE(x) SET_BIT(1, x) +#define CIC_CONTROL_STEREO_MODE(x) SET_BIT(0, x) + +/* CIC_CONFIG bits */ +#define CIC_CONFIG_CIC_SHIFT(x) SET_BITS(27, 24, x) +#define CIC_CONFIG_COMB_COUNT(x) SET_BITS(15, 8, x) + +/* CIC_CONFIG masks */ +#define CIC_CONFIG_CIC_SHIFT_MASK MASK(27, 24) +#define CIC_CONFIG_COMB_COUNT_MASK MASK(15, 8) + +/* MIC_CONTROL bits */ +#define MIC_CONTROL_PDM_EN_B_BIT BIT(1) +#define MIC_CONTROL_PDM_EN_A_BIT BIT(0) +#define MIC_CONTROL_PDM_CLKDIV(x) SET_BITS(15, 8, x) +#define MIC_CONTROL_PDM_SKEW(x) SET_BITS(7, 4, x) +#define MIC_CONTROL_CLK_EDGE(x) SET_BIT(3, x) +#define MIC_CONTROL_PDM_EN_B(x) SET_BIT(1, x) +#define MIC_CONTROL_PDM_EN_A(x) SET_BIT(0, x) + +/* MIC_CONTROL masks */ +#define MIC_CONTROL_PDM_CLKDIV_MASK MASK(15, 8) + +/* FIR_CONTROL_A bits */ +#define FIR_CONTROL_A_START_BIT BIT(7) +#define FIR_CONTROL_A_ARRAY_START_EN_BIT BIT(6) +#define FIR_CONTROL_A_MUTE_BIT BIT(1) +#define FIR_CONTROL_A_START(x) SET_BIT(7, x) +#define FIR_CONTROL_A_ARRAY_START_EN(x) SET_BIT(6, x) +#define FIR_CONTROL_A_DCCOMP(x) SET_BIT(4, x) +#define FIR_CONTROL_A_MUTE(x) SET_BIT(1, x) +#define FIR_CONTROL_A_STEREO(x) SET_BIT(0, x) + +/* FIR_CONFIG_A bits */ +#define FIR_CONFIG_A_FIR_DECIMATION(x) SET_BITS(20, 16, x) +#define FIR_CONFIG_A_FIR_SHIFT(x) SET_BITS(11, 8, x) +#define FIR_CONFIG_A_FIR_LENGTH(x) SET_BITS(7, 0, x) + +/* DC offset compensation time constants */ +#define DCCOMP_TC0 0 +#define DCCOMP_TC1 1 +#define DCCOMP_TC2 2 +#define DCCOMP_TC3 3 +#define DCCOMP_TC4 4 +#define DCCOMP_TC5 5 +#define DCCOMP_TC6 6 +#define DCCOMP_TC7 7 + +/* DC_OFFSET_LEFT_A bits */ +#define DC_OFFSET_LEFT_A_DC_OFFS(x) SET_BITS(21, 0, x) + +/* DC_OFFSET_RIGHT_A bits */ +#define DC_OFFSET_RIGHT_A_DC_OFFS(x) SET_BITS(21, 0, x) + +/* OUT_GAIN_LEFT_A bits */ +#define OUT_GAIN_LEFT_A_GAIN(x) SET_BITS(19, 0, x) + +/* OUT_GAIN_RIGHT_A bits */ +#define OUT_GAIN_RIGHT_A_GAIN(x) SET_BITS(19, 0, x) + +/* FIR_CONTROL_B bits */ +#define FIR_CONTROL_B_START_BIT BIT(7) +#define FIR_CONTROL_B_ARRAY_START_EN_BIT BIT(6) +#define FIR_CONTROL_B_MUTE_BIT BIT(1) +#define FIR_CONTROL_B_START(x) SET_BIT(7, x) +#define FIR_CONTROL_B_ARRAY_START_EN(x) SET_BIT(6, x) +#define FIR_CONTROL_B_DCCOMP(x) SET_BIT(4, x) +#define FIR_CONTROL_B_MUTE(x) SET_BIT(1, x) +#define FIR_CONTROL_B_STEREO(x) SET_BIT(0, x) + +/* FIR_CONFIG_B bits */ +#define FIR_CONFIG_B_FIR_DECIMATION(x) SET_BITS(20, 16, x) +#define FIR_CONFIG_B_FIR_SHIFT(x) SET_BITS(11, 8, x) +#define FIR_CONFIG_B_FIR_LENGTH(x) SET_BITS(7, 0, x) + +/* DC_OFFSET_LEFT_B bits */ +#define DC_OFFSET_LEFT_B_DC_OFFS(x) SET_BITS(21, 0, x) + +/* DC_OFFSET_RIGHT_B bits */ +#define DC_OFFSET_RIGHT_B_DC_OFFS(x) SET_BITS(21, 0, x) + +/* OUT_GAIN_LEFT_B bits */ +#define OUT_GAIN_LEFT_B_GAIN(x) SET_BITS(19, 0, x) + +/* OUT_GAIN_RIGHT_B bits */ +#define OUT_GAIN_RIGHT_B_GAIN(x) SET_BITS(19, 0, x) + +/* FIR coefficients */ +#define FIR_COEF_A(x) SET_BITS(19, 0, x) +#define FIR_COEF_B(x) SET_BITS(19, 0, x) + +/* structs for dmic internal calculations */ +struct dmic_calc_decim_modes { + int16_t clkdiv[DMIC_MAX_MODES]; + int16_t mcic[DMIC_MAX_MODES]; + int16_t mfir[DMIC_MAX_MODES]; + int num_of_modes; +}; + +struct dmic_calc_matched_modes { + int16_t clkdiv[DMIC_MAX_MODES]; + int16_t mcic[DMIC_MAX_MODES]; + int16_t mfir_a[DMIC_MAX_MODES]; + int16_t mfir_b[DMIC_MAX_MODES]; + int num_of_modes; +}; + +struct dmic_calc_configuration { + struct pdm_decim *fir_a; + struct pdm_decim *fir_b; + int clkdiv; + int mcic; + int mfir_a; + int mfir_b; + int cic_shift; + int fir_a_shift; + int fir_b_shift; + int fir_a_length; + int fir_b_length; + int32_t fir_a_scale; + int32_t fir_b_scale; +}; + +/* structs for gathering the parameters from topology */ +struct dmic_config_pdm { + uint16_t id; + uint16_t enable_mic_a; + uint16_t enable_mic_b; + uint16_t polarity_mic_a; + uint16_t polarity_mic_b; + uint16_t clk_edge; + uint16_t skew; +}; + +struct dmic_config_dai { + uint32_t driver_version; + uint32_t io_clk; + uint32_t pdmclk_min; + uint32_t pdmclk_max; + uint32_t fifo_fs; + uint16_t fifo_bits; + uint16_t fifo_bits_b; + uint16_t duty_min; + uint16_t duty_max; + uint32_t num_pdm_active; + uint32_t wake_up_time; + uint32_t min_clock_on_time; + uint32_t unmute_ramp_time; + struct dmic_config_pdm pdm[DMIC_HW_CONTROLLERS]; +}; + +/* every pdm controller has separate fir filter for output fifos */ +struct dmic_calc_fir_coeffs_array { + uint32_t fir_len[DMIC_HW_CONTROLLERS]; + int32_t fir_coeffs[DMIC_HW_CONTROLLERS][DMIC_HW_FIFOS][DMIC_HW_FIR_LENGTH_MAX]; +}; + +struct dmic_config_mic_vendor { + uint8_t type; + uint8_t panel; + uint32_t speaker_position_distance; + uint32_t horizontal_offset; + uint32_t vertical_offset; + uint8_t frequency_low_band; + uint8_t frequency_high_band; + uint16_t direction_angle; + uint16_t elevation_angle; + uint16_t vertical_angle_begin; + uint16_t vertical_angle_end; + uint16_t horizontal_angle_begin; + uint16_t horizontal_angle_end; +}; + +struct dmic_config_mic { + uint8_t num_mics; + uint8_t extension; + int8_t array_type; + uint32_t snr; + uint32_t sensitivity; + struct dmic_config_mic_vendor vendor[8]; +}; + +struct intel_dmic_params { + /* structs to gather dmic params before calculations */ + struct dmic_config_dai dmic_prm[DMIC_HW_FIFOS]; + uint32_t dmic_dai_index; + int dmic_count; + + /* dmic vendor blob structs */ + struct dmic_intel_config_data dmic_blob; + struct dmic_intel_pdm_ctrl_cfg dmic_blob_pdm[DMIC_HW_CONTROLLERS]; + struct dmic_intel_fir_config dmic_blob_fir[DMIC_HW_CONTROLLERS][DMIC_HW_FIFOS]; + struct dmic_calc_fir_coeffs_array dmic_fir_array; + struct dmic_config_mic dmic_mic_config; +}; + +#endif /* __DMIC_MACROS_H */ diff --git a/topology/nhlt/intel/dmic/dmic-process.c b/topology/nhlt/intel/dmic/dmic-process.c new file mode 100644 index 0000000..2db5da4 --- /dev/null +++ b/topology/nhlt/intel/dmic/dmic-process.c @@ -0,0 +1,1378 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include "../intel-nhlt.h" +#include "dmic-process.h" +#include "dmic-internal.h" +#include "pdm-decim-fir.h" +#include "dmic-debug.h" + +/* Note 1: Higher spec filter must be before lower spec filter if there are multiple filters for a + * decimation factor. The first filter is skipped if the length is too much vs. overrun limit. If + * other order the better filter would be never selected. + * + * Note 2: The introduction order of FIR decimation factors is the selection preference order. + * The decimation factor 5 and 10 (2*5) cause a often less compatible output sample rate for CIC so + * they are not used if there other suitable nearby values. + * + * The naming scheme of coefficients set is: + * _____ + */ +struct pdm_decim *fir_list[] = { + &pdm_decim_int32_02_4375_5100_010_095, + &pdm_decim_int32_02_4323_5100_010_095, + &pdm_decim_int32_03_4375_5100_010_095, + &pdm_decim_int32_04_4318_5100_010_095, + &pdm_decim_int32_06_4172_5100_010_095, + &pdm_decim_int32_05_4325_5100_010_095, + &pdm_decim_int32_08_4156_5301_010_090, + &pdm_decim_int32_12_4156_5345_010_090, + &pdm_decim_int32_10_4156_5345_010_090, + NULL, /* This marks the end of coefficients */ +}; + +/* This is a divide function that returns ceil of the quotient. E.g. ceil_divide(9, 3) returns 3, + * ceil_divide(10, 3) returns 4. + */ +static int ceil_divide(int a, int b) +{ + int c; + + c = a / b; + + if (!((a ^ b) & (1U << ((sizeof(int) * 8) - 1))) && c * b != a) + c++; + + return c; +} + +/* This function searches from vec[] (of length vec_length) integer values of n. The indices to + * equal values is returned in idx[]. The function returns the number of found matches. + * The max_results should be set to 0 (or negative) or vec_length to get all matches. The + * max_result can be set to 1 to receive only the first match in ascending order. It avoids need for + * an array for idx. + */ +static int find_equal_int16(int16_t idx[], int16_t vec[], int n, int vec_length, + int max_results) +{ + int nresults = 0; + int i; + + for (i = 0; i < vec_length; i++) { + if (vec[i] == n) { + idx[nresults++] = i; + if (nresults == max_results) + break; + } + } + + return nresults; +} + +/* Return the largest absolute value found in the vector. Note that smallest negative value need to + * be saturated to preset as int32_t. + */ +static int32_t find_max_abs_int32(int32_t vec[], int vec_length) +{ + int i; + int64_t amax = (vec[0] > 0) ? vec[0] : -vec[0]; + + for (i = 1; i < vec_length; i++) { + amax = (vec[i] > amax) ? vec[i] : amax; + amax = (-vec[i] > amax) ? -vec[i] : amax; + } + + return SATP_INT32(amax); /* Amax is always a positive value */ +} + +/* Count the left shift amount to normalize a 32 bit signed integer value without causing overflow. + * Input value 0 will result to 31. + */ +static int norm_int32(int32_t val) +{ + int c = 0; + + /* count number of bits c that val can be right-shifted arithmetically + * until there is -1 (if val is negative) or 0 (if val is positive) + * norm of val will be 31-c + */ + for (; val != -1 && val != 0; c++) + val >>= 1; + + return 31 - c; +} + +/* This function returns a raw list of potential microphone clock and decimation modes for achieving + * requested sample rates. The search is constrained by decimation HW capabililies and setup + * parameters. The parameters such as microphone clock min/max and duty cycle requirements need be + * checked from used microphone component datasheet. + */ +static void find_modes(struct intel_dmic_params *dmic, struct dmic_calc_decim_modes *modes, + uint32_t fs) +{ + int di = dmic->dmic_dai_index; + int clkdiv_min; + int clkdiv_max; + int clkdiv; + int c1; + int du_min; + int du_max; + int pdmclk; + int osr; + int mfir; + int mcic; + unsigned int ioclk_test; + int osr_min = DMIC_MIN_OSR; + int j; + int i = 0; + + /* Defaults, empty result */ + modes->num_of_modes = 0; + + /* The FIFO is not requested if sample rate is set to zero. Just return in such case with + * num_of_modes as zero. + */ + if (fs == 0) { + return; + } + + /* Override DMIC_MIN_OSR for very high sample rates, use as minimum the nominal clock for + * the high rates. + */ + if (fs >= DMIC_HIGH_RATE_MIN_FS) + osr_min = DMIC_HIGH_RATE_OSR_MIN; + + /* Check for sane pdm clock, min 100 kHz, max ioclk/2 */ + if (dmic->dmic_prm[di].pdmclk_max < DMIC_HW_PDM_CLK_MIN || + dmic->dmic_prm[di].pdmclk_max > dmic->dmic_prm[di].io_clk / 2) { + fprintf(stderr, "%s: pdm clock max %d not in range\n", __func__, + dmic->dmic_prm[di].pdmclk_max); + return; + } + if (dmic->dmic_prm[di].pdmclk_min < DMIC_HW_PDM_CLK_MIN || + dmic->dmic_prm[di].pdmclk_min > dmic->dmic_prm[di].pdmclk_max) { + fprintf(stderr, "%s: pdm clock min %d not in range\n", __func__, + dmic->dmic_prm[di].pdmclk_min); + return; + } + + /* Check for sane duty cycle */ + if (dmic->dmic_prm[di].duty_min > dmic->dmic_prm[di].duty_max) { + fprintf(stderr, "%s: duty cycle min > max: %d > %d\n", __func__, + dmic->dmic_prm[di].duty_min, dmic->dmic_prm[di].duty_max); + return; + } + if (dmic->dmic_prm[di].duty_min < DMIC_HW_DUTY_MIN || + dmic->dmic_prm[di].duty_min > DMIC_HW_DUTY_MAX) { + fprintf(stderr, "%s: pdm clock min %d not in range\n", __func__, + dmic->dmic_prm[di].duty_min); + return; + } + if (dmic->dmic_prm[di].duty_max < DMIC_HW_DUTY_MIN || + dmic->dmic_prm[di].duty_max > DMIC_HW_DUTY_MAX) { + fprintf(stderr, "%s: pdm clock max %d not in range\n", __func__, + dmic->dmic_prm[di].duty_max); + return; + } + + /* Min and max clock dividers */ + clkdiv_min = ceil_divide(dmic->dmic_prm[di].io_clk, dmic->dmic_prm[di].pdmclk_max); + clkdiv_min = MAX(clkdiv_min, DMIC_HW_CIC_DECIM_MIN); + clkdiv_max = dmic->dmic_prm[di].io_clk / dmic->dmic_prm[di].pdmclk_min; + + /* Loop possible clock dividers and check based on resulting oversampling ratio that CIC and + * FIR decimation ratios are feasible. The ratios need to be integers. Also the mic clock + * duty cycle need to be within limits. + */ + for (clkdiv = clkdiv_min; clkdiv <= clkdiv_max; clkdiv++) { + /* Calculate duty cycle for this clock divider. Note that odd dividers cause non-50% + * duty cycle. + */ + c1 = clkdiv >> 1; + du_min = 100 * c1 / clkdiv; + du_max = 100 - du_min; + + /* Calculate PDM clock rate and oversampling ratio. */ + pdmclk = dmic->dmic_prm[di].io_clk / clkdiv; + osr = pdmclk / fs; + + /* Check that OSR constraints is met and clock duty cycle does not exceed microphone + * specification. If exceed proceed to next clkdiv. + */ + if (osr < osr_min || du_min < dmic->dmic_prm[di].duty_min || + du_max > dmic->dmic_prm[di].duty_max) + continue; + + /* Loop FIR decimation factors candidates. If the integer divided decimation factors + * and clock dividers as multiplied with sample rate match the IO clock rate the + * division was exact and such decimation mode is possible. Then check that CIC + * decimation constraints are met. The passed decimation modes are added to array. + */ + for (j = 0; fir_list[j]; j++) { + mfir = fir_list[j]->decim_factor; + + /* Skip if previous decimation factor was the same */ + if (j > 1 && fir_list[j - 1]->decim_factor == mfir) + continue; + + mcic = osr / mfir; + ioclk_test = fs * mfir * mcic * clkdiv; + + if (ioclk_test == dmic->dmic_prm[di].io_clk && + mcic >= DMIC_HW_CIC_DECIM_MIN && + mcic <= DMIC_HW_CIC_DECIM_MAX && + i < DMIC_MAX_MODES) { + modes->clkdiv[i] = clkdiv; + modes->mcic[i] = mcic; + modes->mfir[i] = mfir; + i++; + } + } + } + + modes->num_of_modes = i; +} + +/* The previous raw modes list contains sane configuration possibilities. When there is request for + * both FIFOs A and B operation this function returns list of compatible settings. + */ +static void match_modes(struct dmic_calc_matched_modes *c, struct dmic_calc_decim_modes *a, + struct dmic_calc_decim_modes *b) +{ + int16_t idx[DMIC_MAX_MODES]; + int idx_length; + int i; + int n; + int m; + + /* Check if previous search got results. */ + c->num_of_modes = 0; + if (a->num_of_modes == 0 && b->num_of_modes == 0) { + /* Nothing to do */ + return; + } + + /* Ensure that num_of_modes is sane. */ + if (a->num_of_modes > DMIC_MAX_MODES || + b->num_of_modes > DMIC_MAX_MODES) + return; + + /* Check for request only for FIFO A or B. In such case pass list for A or B as such. */ + if (b->num_of_modes == 0) { + c->num_of_modes = a->num_of_modes; + for (i = 0; i < a->num_of_modes; i++) { + c->clkdiv[i] = a->clkdiv[i]; + c->mcic[i] = a->mcic[i]; + c->mfir_a[i] = a->mfir[i]; + c->mfir_b[i] = 0; /* Mark FIR B as non-used */ + } + return; + } + + if (a->num_of_modes == 0) { + c->num_of_modes = b->num_of_modes; + for (i = 0; i < b->num_of_modes; i++) { + c->clkdiv[i] = b->clkdiv[i]; + c->mcic[i] = b->mcic[i]; + c->mfir_b[i] = b->mfir[i]; + c->mfir_a[i] = 0; /* Mark FIR A as non-used */ + } + return; + } + + /* Merge a list of compatible modes */ + i = 0; + for (n = 0; n < a->num_of_modes; n++) { + /* Find all indices of values a->clkdiv[n] in b->clkdiv[] */ + idx_length = find_equal_int16(idx, b->clkdiv, a->clkdiv[n], + b->num_of_modes, 0); + for (m = 0; m < idx_length; m++) { + if (b->mcic[idx[m]] == a->mcic[n]) { + c->clkdiv[i] = a->clkdiv[n]; + c->mcic[i] = a->mcic[n]; + c->mfir_a[i] = a->mfir[n]; + c->mfir_b[i] = b->mfir[idx[m]]; + i++; + } + } + c->num_of_modes = i; + } +} + +/* Finds a suitable FIR decimation filter from the included set */ +static struct pdm_decim *get_fir(struct intel_dmic_params *dmic, + struct dmic_calc_configuration *cfg, int mfir) +{ + int i = 0; + int fs; + int cic_fs; + int fir_max_length; + struct pdm_decim *fir = NULL; + int di = dmic->dmic_dai_index; + + if (mfir <= 0) + return fir; + + cic_fs = dmic->dmic_prm[di].io_clk / cfg->clkdiv / cfg->mcic; + fs = cic_fs / mfir; + /* FIR max. length depends on available cycles and coef RAM length. Exceeding this length + * sets HW overrun status and overwrite of other register. + */ + fir_max_length = MIN(DMIC_HW_FIR_LENGTH_MAX, + (int)dmic->dmic_prm[di].io_clk / fs / 2 - + DMIC_FIR_PIPELINE_OVERHEAD); + + /* Loop until NULL */ + while (fir_list[i]) { + if (fir_list[i]->decim_factor == mfir) { + if (fir_list[i]->length <= fir_max_length) { + /* Store pointer, break from loop to avoid a possible other mode + * with lower FIR length. + */ + fir = fir_list[i]; + break; + } + } + i++; + } + + return fir; +} + +/* Calculate scale and shift to use for FIR coefficients. Scale is applied before write to HW coef + * RAM. Shift will be programmed to HW register. + */ +static int fir_coef_scale(int32_t *fir_scale, int *fir_shift, int add_shift, + const int32_t coef[], int coef_length, int32_t gain) +{ + int32_t amax; + int32_t new_amax; + int32_t fir_gain; + int shift; + + /* Multiply gain passed from CIC with output full scale. */ + fir_gain = Q_MULTSR_32X32((int64_t)gain, DMIC_HW_SENS_Q28, + DMIC_FIR_SCALE_Q, 28, DMIC_FIR_SCALE_Q); + + /* Find the largest FIR coefficient value. */ + amax = find_max_abs_int32((int32_t *)coef, coef_length); + + /* Scale max. tap value with FIR gain. */ + new_amax = Q_MULTSR_32X32((int64_t)amax, fir_gain, 31, + DMIC_FIR_SCALE_Q, DMIC_FIR_SCALE_Q); + if (new_amax <= 0) + return -EINVAL; + + /* Get left shifts count to normalize the fractional value as 32 bit. We need right shifts + * count for scaling so need to invert. The difference of Q31 vs. used Q format is added to + * get the correct normalization right shift value. + */ + shift = 31 - DMIC_FIR_SCALE_Q - norm_int32(new_amax); + + /* Add to shift for coef raw Q31 format shift and store to configuration. Ensure range (fail + * should not happen with OK coefficient set). + */ + *fir_shift = -shift + add_shift; + if (*fir_shift < DMIC_HW_FIR_SHIFT_MIN || + *fir_shift > DMIC_HW_FIR_SHIFT_MAX) + return -EINVAL; + + /* Compensate shift into FIR coef scaler and store as Q4.20. */ + if (shift < 0) + *fir_scale = fir_gain << -shift; + else + *fir_scale = fir_gain >> shift; + + return 0; +} + +/* This function selects with a simple criteria one mode to set up the decimator. For the settings + * chosen for FIFOs A and B output a lookup is done for FIR coefficients from the included + * coefficients tables. For some decimation factors there may be several length coefficient sets. It + * is due to possible restruction of decimation engine cycles per given sample rate. If the + * coefficients length is exceeded the lookup continues. Therefore the list of coefficient set must + * present the filters for a decimation factor in decreasing length order. + * + * Note: If there is no filter available an error is returned. The parameters should be reviewed for + * such case. If still a filter is missing it should be added into the included set. FIR decimation + * with a high factor usually needs compromizes into specifications and is not desirable. + */ +static int select_mode(struct intel_dmic_params *dmic, struct dmic_calc_configuration *cfg, + struct dmic_calc_matched_modes *modes) +{ + int32_t g_cic; + int32_t fir_in_max; + int32_t cic_out_max; + int32_t gain_to_fir; + int16_t idx[DMIC_MAX_MODES]; + int16_t *mfir; + int mcic; + int bits_cic; + int ret; + int n; + int found = 0; + + /* If there are more than one possibilities select a mode with a preferred FIR decimation + * factor. If there are several select mode with highest ioclk divider to minimize + * microphone power consumption. The highest clock divisors are in the end of list so select + * the last of list. The minimum OSR criteria used in previous ensures that quality in the + * candidates should be sufficient. + */ + if (modes->num_of_modes == 0) { + fprintf(stderr, "%s: no modes available\n", __func__); + return -EINVAL; + } + + /* Valid modes presence is indicated with non-zero decimation factor in 1st element. If FIR + * A is not used get decimation factors from FIR B instead. + */ + if (modes->mfir_a[0] > 0) + mfir = modes->mfir_a; + else + mfir = modes->mfir_b; + + /* Search fir_list[] decimation factors from start towards end. The found last configuration + * entry with searched decimation factor will be used. + */ + for (n = 0; fir_list[n]; n++) { + found = find_equal_int16(idx, mfir, fir_list[n]->decim_factor, + modes->num_of_modes, 0); + if (found) + break; + } + + if (!found) { + fprintf(stderr, "%s: No filter for decimation found\n", __func__); + return -EINVAL; + } + n = idx[found - 1]; /* Option with highest clock divisor and lowest mic clock rate */ + + /* Get microphone clock and decimation parameters for used mode from the list. */ + cfg->clkdiv = modes->clkdiv[n]; + cfg->mfir_a = modes->mfir_a[n]; + cfg->mfir_b = modes->mfir_b[n]; + cfg->mcic = modes->mcic[n]; + cfg->fir_a = NULL; + cfg->fir_b = NULL; + + /* Find raw FIR coefficients to match the decimation factors of FIR A and B. */ + if (cfg->mfir_a > 0) { + cfg->fir_a = get_fir(dmic, cfg, cfg->mfir_a); + if (!cfg->fir_a) { + fprintf(stderr, "%s: can't find FIR coefficients, mfir_a = %d\n", + __func__, cfg->mfir_a); + return -EINVAL; + } + } + + if (cfg->mfir_b > 0) { + cfg->fir_b = get_fir(dmic, cfg, cfg->mfir_b); + if (!cfg->fir_b) { + fprintf(stderr, "%s: can't find FIR coefficients, mfir_b = %d\n", + __func__, cfg->mfir_b); + return -EINVAL; + } + } + + /* Calculate CIC shift from the decimation factor specific gain. The gain of HW decimator + * equals decimation factor to power of 5. + */ + mcic = cfg->mcic; + g_cic = mcic * mcic * mcic * mcic * mcic; + if (g_cic < 0) { + /* Erroneous decimation factor and CIC gain */ + fprintf(stderr, "%s: erroneous decimation factor and CIC gain\n", __func__); + return -EINVAL; + } + + bits_cic = 32 - norm_int32(g_cic); + cfg->cic_shift = bits_cic - DMIC_HW_BITS_FIR_INPUT; + + /* Calculate remaining gain to FIR in Q format used for gain values. */ + fir_in_max = INT_MAX(DMIC_HW_BITS_FIR_INPUT); + if (cfg->cic_shift >= 0) + cic_out_max = g_cic >> cfg->cic_shift; + else + cic_out_max = g_cic << -cfg->cic_shift; + + gain_to_fir = (int32_t)((((int64_t)fir_in_max) << DMIC_FIR_SCALE_Q) / + cic_out_max); + + /* Calculate FIR scale and shift */ + if (cfg->mfir_a > 0) { + cfg->fir_a_length = cfg->fir_a->length; + ret = fir_coef_scale(&cfg->fir_a_scale, &cfg->fir_a_shift, + cfg->fir_a->shift, cfg->fir_a->coef, + cfg->fir_a->length, gain_to_fir); + if (ret < 0) { + /* Invalid coefficient set found, should not happen. */ + fprintf(stderr, "%s: invalid coefficient set found\n", __func__); + return -EINVAL; + } + } else { + cfg->fir_a_scale = 0; + cfg->fir_a_shift = 0; + cfg->fir_a_length = 0; + } + + if (cfg->mfir_b > 0) { + cfg->fir_b_length = cfg->fir_b->length; + ret = fir_coef_scale(&cfg->fir_b_scale, &cfg->fir_b_shift, + cfg->fir_b->shift, cfg->fir_b->coef, + cfg->fir_b->length, gain_to_fir); + if (ret < 0) { + /* Invalid coefficient set found, should not happen. */ + fprintf(stderr, "%s: invalid coefficient set found\n", __func__); + return -EINVAL; + } + } else { + cfg->fir_b_scale = 0; + cfg->fir_b_shift = 0; + cfg->fir_b_length = 0; + } + + return 0; +} + +/* The FIFO input packer mode (IPM) settings are somewhat different in HW versions. This helper + * function returns a suitable IPM bit field value to use. + */ +static void ipm_helper1(struct intel_dmic_params *dmic, int *ipm) +{ + int di = dmic->dmic_dai_index; + int pdm[DMIC_HW_CONTROLLERS]; + int i; + + /* Loop number of PDM controllers in the configuration. If mic A or B is enabled then a pdm + * controller is marked as active for this DAI. + */ + for (i = 0; i < DMIC_HW_CONTROLLERS; i++) { + if (dmic->dmic_prm[di].pdm[i].enable_mic_a || + dmic->dmic_prm[di].pdm[i].enable_mic_b) + pdm[i] = 1; + else + pdm[i] = 0; + } + + /* Set IPM to match active pdm controllers. */ + *ipm = 0; + + if (pdm[0] == 0 && pdm[1] > 0) + *ipm = 1; + + if (pdm[0] > 0 && pdm[1] > 0) + *ipm = 2; +} + +static void ipm_helper2(struct intel_dmic_params *dmic, int source[], int *ipm) +{ + int di = dmic->dmic_dai_index; + int pdm[DMIC_HW_CONTROLLERS]; + int i; + int n = 0; + + for (i = 0; i < OUTCONTROLX_IPM_NUMSOURCES; i++) + source[i] = 0; + + /* Loop number of PDM controllers in the configuration. If mic A or B is enabled then a pdm + * controller is marked as active. The function returns in array source[] the indice of + * enabled pdm controllers to be used for IPM configuration. + */ + for (i = 0; i < DMIC_HW_CONTROLLERS; i++) { + if (dmic->dmic_prm[di].pdm[i].enable_mic_a || + dmic->dmic_prm[di].pdm[i].enable_mic_b) { + pdm[i] = 1; + source[n] = i; + n++; + } else { + pdm[i] = 0; + } + } + + /* IPM bit field is set to count of active pdm controllers. */ + *ipm = pdm[0]; + for (i = 1; i < DMIC_HW_CONTROLLERS; i++) + *ipm += pdm[i]; +} + +/* Loop number of PDM controllers in the configuration. The function checks if the controller should + * operate as stereo or mono left (A) or mono right (B) mode. Mono right mode is setup as channel + * swapped mono left. + */ +static int stereo_helper(struct intel_dmic_params *dmic, int stereo[], int swap[], int ipmsm[]) +{ + int cnt; + int i, j; + int swap_check; + int ret = 0; + + /* Find FIFOs those need stereo mode, ipmsm 0 is mono, 1 is stereo */ + for (i = 0; i < DMIC_HW_FIFOS; i++) { + ipmsm[i] = 0; + for (j = 0; j < DMIC_HW_CONTROLLERS; j++) { + if (dmic->dmic_prm[i].pdm[j].enable_mic_a && dmic->dmic_prm[i].pdm[j].enable_mic_b) { + ipmsm[i] = 1; + break; + } + } + } + + for (i = 0; i < DMIC_HW_CONTROLLERS; i++) { + cnt = 0; + if (dmic->dmic_prm[0].pdm[i].enable_mic_a || + dmic->dmic_prm[1].pdm[i].enable_mic_a) + cnt++; + + if (dmic->dmic_prm[0].pdm[i].enable_mic_b || + dmic->dmic_prm[1].pdm[i].enable_mic_b) + cnt++; + + /* Set stereo mode if both mic A anc B are enabled. */ + cnt >>= 1; + stereo[i] = cnt; + + /* Swap channels if only mic B is used for mono processing. */ + swap[i] = (dmic->dmic_prm[0].pdm[i].enable_mic_b || + dmic->dmic_prm[1].pdm[i].enable_mic_b) && !cnt; + + /* Check that swap does not conflict with other DAI request */ + swap_check = (dmic->dmic_prm[1].pdm[i].enable_mic_a || + dmic->dmic_prm[0].pdm[i].enable_mic_a); + + if (swap_check && swap[i]) { + ret = -EINVAL; + break; + } + } + return ret; +} + +static int configure_registers(struct intel_dmic_params *dmic, struct dmic_calc_configuration *cfg) +{ + int ipmsm[DMIC_HW_FIFOS]; + int stereo[DMIC_HW_CONTROLLERS]; + int swap[DMIC_HW_CONTROLLERS]; + uint32_t val = 0; + int32_t ci; + uint32_t cu; + int ipm; + int of0; + int of1; + int fir_decim; + int fir_length; + int length; + int edge; + int soft_reset; + int cic_mute; + int fir_mute; + unsigned int i; + int j; + int ret; + int mic; + int chmap_bits; + int di = dmic->dmic_dai_index; + int dccomp = 1; + int array_a = 0; + int array_b = 0; + int bfth = 3; /* Should be 3 for 8 entries, 1 is 2 entries */ + int th = 3; /* Used with TIE=1 */ + int source[OUTCONTROLX_IPM_NUMSOURCES]; + + /* + * ts_group value describes which audio channels in the hw fifo are enabled. A 32 bit + * value is divided into 8 x 4 bit nibbles corresponding to 8 audio channels. Hex value 0xF + * means "not in use", any other value means the channel is enabled. For example 0xFFFFFFFF + * means no channels are enabled, 0xFFFFFF10 means channels 1 and 2 are enabled. + * + * ts_group array index corresponds to dmic hw fifos, that gather audio samples from pdm + * controllers. 1 pdm controller can host 2 mono dmics and usually pdm controllers are + * connected to 2 hw fifos -> we can for example run the dmics simultaneously with different + * sampling rates. + * + * Currently there is no evidence we would ever have more than 2 hw fifos, so ts_group[2] + * and ts_group[3] are not used for anything. Also the nibbles could be used for channel + * mapping the pdm channels arbitrarely into hw fifos, however currently it is used as + * binary not_enabled/enabled setting. + * + * if we have 2 dmics (stereo) it means we are using 1 pdm controller with possibly 2 hw + * fifos: + * mic1 fifo0(2ch) + * \ / + * pdm0 + * / \ + * mic2 fifo1(2ch) + * + * So in this case it makes only sense to control ts_group indexes 0 and 1 and their last 2 + * nibbles (as we have only 2 channels). + * + * if we have 4 dmics, it means we are using 2 pdm controller with possibly 2 x 4 channel hw + * fifos: + * + * mic1 fifo0(4ch) + * \ / / + * pdm0 / + * / \ / + * mic2 \/ + * mic3 /\ + * \ / \ + * pdm1 \ + * / \ \ + * mic4 fifo1(4ch) + * + * So it makes sense to control ts_group indexes 0 and 1 and their last 4 nibbles. + * + * channel_pdm_mask defines which existing pdm controllers will be taken into use. So if + * either of mic a or b is enabled -> that particular pdm controller is in use. For example + * pdm0 in use/not_in_use is defined by setting bit 0 in channel_pdm_mask to 1/0. + * + * channel_ctrl_mask defines what mic channels are available in hw for a pdm controller. in + * theory pdm controller could have only 1 channel enabled, in practice there's always 2 + * channels which are both enabled -> set bits 0 and 1. + */ + + for (i = 0, mic = 0, chmap_bits = 4; i < DMIC_HW_CONTROLLERS; i++) { + /* enable fifo channels (ts_group) based on mic_enable in dai definition */ + if (dmic->dmic_prm[di].pdm[i].enable_mic_a) { + dmic->dmic_blob.ts_group[di] &= ~(0xF << (chmap_bits * mic)); + dmic->dmic_blob.ts_group[di] |= 0x0 << (chmap_bits * mic); + } + mic++; + if (dmic->dmic_prm[di].pdm[i].enable_mic_b) { + dmic->dmic_blob.ts_group[di] &= ~(0xF << (chmap_bits * mic)); + dmic->dmic_blob.ts_group[di] |= 0x1 << (chmap_bits * mic); + } + mic++; + } + + /* set channel_pdm_mask to describe what pdm controllers are in use */ + for (i = 0; i < dmic->dmic_prm[di].num_pdm_active; i++) + dmic->dmic_blob.channel_pdm_mask |= 1 << i; + + /* set always both mic channels enabled */ + dmic->dmic_blob.channel_ctrl_mask = 0x3; + + /* Normal start sequence */ + soft_reset = 0; + cic_mute = 0; + fir_mute = 0; + + /* OUTCONTROL0 and OUTCONTROL1 */ + of0 = (dmic->dmic_prm[0].fifo_bits == 32) ? 2 : 0; + of1 = (dmic->dmic_prm[1].fifo_bits == 32) ? 2 : 0; + + ret = stereo_helper(dmic, stereo, swap, ipmsm); + if (ret < 0) { + fprintf(stderr, "%s: Microphones enable conflict for DMIC0 and DMIC1 FIFOs\n", __func__); + return ret; + } + + if (dmic->dmic_prm[di].driver_version == 1) { + if (di == 0) { + ipm_helper1(dmic, &ipm); + val = OUTCONTROL0_TIE(0) | + OUTCONTROL0_SIP(0) | + OUTCONTROL0_FINIT(0) | + OUTCONTROL0_FCI(0) | + OUTCONTROL0_BFTH(bfth) | + OUTCONTROL0_OF(of0) | + OUTCONTROL0_IPM_VER1(ipm) | + OUTCONTROL0_TH(th); + } else { + ipm_helper1(dmic, &ipm); + val = OUTCONTROL1_TIE(0) | + OUTCONTROL1_SIP(0) | + OUTCONTROL1_FINIT(0) | + OUTCONTROL1_FCI(0) | + OUTCONTROL1_BFTH(bfth) | + OUTCONTROL1_OF(of1) | + OUTCONTROL1_IPM_VER1(ipm) | + OUTCONTROL1_TH(th); + } + } + + if (dmic->dmic_prm[di].driver_version >= 2) { + if (dmic->dmic_prm[di].driver_version >= 4) + bfth = 0; + + if (di == 0) { + ipm_helper2(dmic, source, &ipm); + val = OUTCONTROL0_TIE(0) | + OUTCONTROL0_SIP(0) | + OUTCONTROL0_FINIT(0) | + OUTCONTROL0_FCI(0) | + OUTCONTROL0_BFTH(bfth) | + OUTCONTROL0_OF(of0) | + OUTCONTROL0_IPM_VER2(ipm) | + OUTCONTROL0_IPM_SOURCE_1(source[0]) | + OUTCONTROL0_IPM_SOURCE_2(source[1]) | + OUTCONTROL0_IPM_SOURCE_3(source[2]) | + OUTCONTROL0_IPM_SOURCE_4(source[3]) | + OUTCONTROL0_IPM_SOURCE_MODE(ipmsm[0]) | + OUTCONTROL0_TH(th); + } else { + ipm_helper2(dmic, source, &ipm); + val = OUTCONTROL1_TIE(0) | + OUTCONTROL1_SIP(0) | + OUTCONTROL1_FINIT(0) | + OUTCONTROL1_FCI(0) | + OUTCONTROL1_BFTH(bfth) | + OUTCONTROL1_OF(of1) | + OUTCONTROL1_IPM_VER2(ipm) | + OUTCONTROL1_IPM_SOURCE_1(source[0]) | + OUTCONTROL1_IPM_SOURCE_2(source[1]) | + OUTCONTROL1_IPM_SOURCE_3(source[2]) | + OUTCONTROL1_IPM_SOURCE_4(source[3]) | + OUTCONTROL1_IPM_SOURCE_MODE(ipmsm[1]) | + OUTCONTROL1_TH(th); + } + } + + dmic->dmic_blob.chan_ctrl_cfg[di] = val; + + for (i = 0; i < DMIC_HW_CONTROLLERS; i++) { + /* CIC */ + val = CIC_CONTROL_SOFT_RESET(soft_reset) | + CIC_CONTROL_CIC_START_B(1) | + CIC_CONTROL_CIC_START_A(1) | + CIC_CONTROL_MIC_B_POLARITY(dmic->dmic_prm[di].pdm[i].polarity_mic_b) | + CIC_CONTROL_MIC_A_POLARITY(dmic->dmic_prm[di].pdm[i].polarity_mic_a) | + CIC_CONTROL_MIC_MUTE(cic_mute); + + if (dmic->dmic_prm[di].driver_version == 1) + val |= CIC_CONTROL_STEREO_MODE(stereo[i]); + + dmic->dmic_blob_pdm[i].cic_control = val; + + val = CIC_CONFIG_CIC_SHIFT(cfg->cic_shift + 8) | + CIC_CONFIG_COMB_COUNT(cfg->mcic - 1); + dmic->dmic_blob_pdm[i].cic_config = val; + + /* Mono right channel mic usage requires swap of PDM channels + * since the mono decimation is done with only left channel + * processing active. + */ + edge = dmic->dmic_prm[di].pdm[i].clk_edge; + if (swap[i]) + edge = !edge; + + val = MIC_CONTROL_PDM_CLKDIV(cfg->clkdiv - 2) | + MIC_CONTROL_PDM_SKEW(dmic->dmic_prm[di].pdm[i].skew) | + MIC_CONTROL_CLK_EDGE(edge) | + MIC_CONTROL_PDM_EN_B(1) | + MIC_CONTROL_PDM_EN_A(1); + dmic->dmic_blob_pdm[i].mic_control = val; + + /* + * Here we have to check the both FIRs if they are + * configured as the later configured DAI may have changed + * the configuration of the DAI configured earlier. + */ + if (cfg->mfir_a) { + /* FIR A */ + fir_decim = MAX(cfg->mfir_a - 1, 0); + fir_length = MAX(cfg->fir_a_length - 1, 0); + val = FIR_CONTROL_A_START(1) | + FIR_CONTROL_A_ARRAY_START_EN(array_a) | + FIR_CONTROL_A_DCCOMP(dccomp) | + FIR_CONTROL_A_MUTE(fir_mute) | + FIR_CONTROL_A_STEREO(stereo[i]); + dmic->dmic_blob_fir[i][0].fir_control = val; + + val = FIR_CONFIG_A_FIR_DECIMATION(fir_decim) | + FIR_CONFIG_A_FIR_SHIFT(cfg->fir_a_shift) | + FIR_CONFIG_A_FIR_LENGTH(fir_length); + dmic->dmic_blob_fir[i][0].fir_config = val; + + val = DC_OFFSET_LEFT_A_DC_OFFS(DCCOMP_TC0); + dmic->dmic_blob_fir[i][0].dc_offset_left = val; + + val = DC_OFFSET_RIGHT_A_DC_OFFS(DCCOMP_TC0); + dmic->dmic_blob_fir[i][0].dc_offset_right = val; + + val = OUT_GAIN_LEFT_A_GAIN(0); + dmic->dmic_blob_fir[i][0].out_gain_left = val; + + val = OUT_GAIN_RIGHT_A_GAIN(0); + dmic->dmic_blob_fir[i][0].out_gain_right = val; + + /* Write coef RAM A with scaled coefficient in reverse order */ + length = cfg->fir_a_length; + for (j = 0; j < length; j++) { + ci = (int32_t)Q_MULTSR_32X32((int64_t)cfg->fir_a->coef[j], + cfg->fir_a_scale, 31, + DMIC_FIR_SCALE_Q, DMIC_HW_FIR_COEF_Q); + cu = FIR_COEF_A(ci); + /* blob_pdm[i].fir_coeffs[0][j] = cu; */ + dmic->dmic_fir_array.fir_coeffs[i][0][j] = cu; + } + dmic->dmic_fir_array.fir_len[0] = length; + } else { + dmic->dmic_fir_array.fir_len[0] = 0; + } + + if (cfg->mfir_b) { + /* FIR B */ + fir_decim = MAX(cfg->mfir_b - 1, 0); + fir_length = MAX(cfg->fir_b_length - 1, 0); + val = FIR_CONTROL_B_START(1) | + FIR_CONTROL_B_ARRAY_START_EN(array_b) | + FIR_CONTROL_B_DCCOMP(dccomp) | + FIR_CONTROL_B_MUTE(fir_mute) | + FIR_CONTROL_B_STEREO(stereo[i]); + dmic->dmic_blob_fir[i][1].fir_control = val; + + val = FIR_CONFIG_B_FIR_DECIMATION(fir_decim) | + FIR_CONFIG_B_FIR_SHIFT(cfg->fir_b_shift) | + FIR_CONFIG_B_FIR_LENGTH(fir_length); + dmic->dmic_blob_fir[i][1].fir_config = val; + val = DC_OFFSET_LEFT_B_DC_OFFS(DCCOMP_TC0); + dmic->dmic_blob_fir[i][1].dc_offset_left = val; + + val = DC_OFFSET_RIGHT_B_DC_OFFS(DCCOMP_TC0); + dmic->dmic_blob_fir[i][1].dc_offset_right = val; + + val = OUT_GAIN_LEFT_B_GAIN(0); + dmic->dmic_blob_fir[i][1].out_gain_left = val; + + val = OUT_GAIN_RIGHT_B_GAIN(0); + dmic->dmic_blob_fir[i][1].out_gain_right = val; + + /* Write coef RAM B with scaled coefficient in reverse order */ + length = cfg->fir_b_length; + for (j = 0; j < length; j++) { + ci = (int32_t)Q_MULTSR_32X32((int64_t)cfg->fir_b->coef[j], + cfg->fir_b_scale, 31, + DMIC_FIR_SCALE_Q, DMIC_HW_FIR_COEF_Q); + cu = FIR_COEF_B(ci); + /* blob_pdm[i].fir_coeffs[1][j] = cu; */ + dmic->dmic_fir_array.fir_coeffs[i][1][j] = cu; + } + dmic->dmic_fir_array.fir_len[1] = length; + } else { + dmic->dmic_fir_array.fir_len[1] = 0; + } + } + + return 0; +} + +/* The decimation for PDM (pulse density modulation) stream is done in a programmable HW filter + * engine. The input to configuration algorithm is needed sample rate, channels/enabled microphones, + * microphone clock range, microphone clock duty cycle range, and system clock rate. + * + * The PDM bus clock divider, CIC and FIR decimation ratios are searched and configuration for + * optimal power consumption, filtering requirements, and HW constraints is chosen. The FIR filter + * for the chosen decimation is looked up from table and scaled to match the other decimation path + * sensitivity. + */ +int dmic_calculate(struct intel_nhlt_params *nhlt) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + struct dmic_calc_matched_modes modes_ab; + struct dmic_calc_decim_modes modes_a; + struct dmic_calc_decim_modes modes_b; + struct dmic_calc_configuration cfg; + int ret = 0; + int di; + + if (!dmic) + return -EINVAL; + + di = dmic->dmic_dai_index; + + if (di >= DMIC_HW_FIFOS) { + fprintf(stderr, "%s: dai->index %d exceeds number of FIFOs\n", __func__, di); + ret = -EINVAL; + goto out; + } + + if (dmic->dmic_prm[di].num_pdm_active > DMIC_HW_CONTROLLERS) { + fprintf(stderr, "%s: controller count %d exceeds platform capability\n", + __func__, dmic->dmic_prm[di].num_pdm_active); + ret = -EINVAL; + goto out; + } + + /* fifo bits 0 means fifo disabled */ + switch (dmic->dmic_prm[di].fifo_bits) { + case 0: + case 16: + case 32: + break; + default: + fprintf(stderr, "%s: Bad fifo_bits %d\n", __func__, + dmic->dmic_prm[di].fifo_bits); + ret = -EINVAL; + goto out; + } + + /* Match and select optimal decimators configuration for FIFOs A and B paths. This setup + * phase is still abstract. Successful completion points struct cfg to FIR coefficients and + * contains the scale value to use for FIR coefficient RAM write as well as the CIC and FIR + * shift values. + */ + find_modes(dmic, &modes_a, dmic->dmic_prm[0].fifo_fs); + if (modes_a.num_of_modes == 0 && dmic->dmic_prm[0].fifo_fs > 0) { + fprintf(stderr, "%s: No modes found for FIFO A\n", __func__); + ret = -EINVAL; + goto out; + } + + find_modes(dmic, &modes_b, dmic->dmic_prm[1].fifo_fs); + if (modes_b.num_of_modes == 0 && dmic->dmic_prm[1].fifo_fs > 0) { + fprintf(stderr, "%s: No modes found for FIFO B\n", __func__); + ret = -EINVAL; + goto out; + } + + match_modes(&modes_ab, &modes_a, &modes_b); + ret = select_mode(dmic, &cfg, &modes_ab); + if (ret < 0) { + fprintf(stderr, "%s: select_mode() failed %d\n", __func__, ret); + ret = -EINVAL; + goto out; + } + + /* Struct reg contains a mirror of actual HW registers. Determine register bits + * configuration from decimator configuration and the requested parameters. + */ + ret = configure_registers(dmic, &cfg); + if (ret < 0) { + fprintf(stderr, "%s: cannot configure registers %d\n", __func__, ret); + ret = -EINVAL; + goto out; + } + + dmic_print_internal(dmic); + + dmic->dmic_count++; + +out: + return ret; +} + +int dmic_get_params(struct intel_nhlt_params *nhlt, int index, uint32_t *sample_rate, + uint16_t *channel_count, uint32_t *bits_per_sample, uint8_t *array_type, + uint8_t *num_mics, uint8_t *extension, uint32_t *snr, uint32_t *sensitivity) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + uint32_t channels = 0; + + if (!dmic) + return -EINVAL; + + /* check all pdm's for enabled mics */ + *channel_count = 0; + if (dmic->dmic_prm[index].pdm[0].enable_mic_a) + channels++; + + if (dmic->dmic_prm[index].pdm[0].enable_mic_b) + channels++; + + if (dmic->dmic_prm[index].pdm[1].enable_mic_a) + channels++; + + if (dmic->dmic_prm[index].pdm[1].enable_mic_b) + channels++; + + *sample_rate = dmic->dmic_prm[index].fifo_fs; + *channel_count = channels; + *bits_per_sample = dmic->dmic_prm[index].fifo_bits; + *num_mics = dmic->dmic_mic_config.num_mics; + *extension = dmic->dmic_mic_config.extension; + *array_type = dmic->dmic_mic_config.array_type; + *snr = dmic->dmic_mic_config.snr; + *sensitivity = dmic->dmic_mic_config.sensitivity; + + return 0; +} + +int dmic_get_mic_params(struct intel_nhlt_params *nhlt, int index, + uint8_t *type, uint8_t *panel, uint32_t *speaker_position_distance, + uint32_t *horizontal_offset, uint32_t *vertical_offset, + uint8_t *frequency_low_band, uint8_t *frequency_high_band, + uint16_t *direction_angle, uint16_t *elevation_angle, + uint16_t *vertical_angle_begin, uint16_t *vertical_angle_end, + uint16_t *horizontal_angle_begin, uint16_t *horizontal_angle_end) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + + if (!dmic) + return -EINVAL; + + *type = dmic->dmic_mic_config.vendor[index].type; + *panel = dmic->dmic_mic_config.vendor[index].panel; + *speaker_position_distance = dmic->dmic_mic_config.vendor[index].speaker_position_distance; + *horizontal_offset = dmic->dmic_mic_config.vendor[index].horizontal_offset; + *vertical_offset = dmic->dmic_mic_config.vendor[index].vertical_offset; + *frequency_low_band = dmic->dmic_mic_config.vendor[index].frequency_low_band; + *frequency_high_band = dmic->dmic_mic_config.vendor[index].frequency_high_band; + *direction_angle = dmic->dmic_mic_config.vendor[index].direction_angle; + *elevation_angle = dmic->dmic_mic_config.vendor[index].elevation_angle; + *vertical_angle_begin = dmic->dmic_mic_config.vendor[index].vertical_angle_begin; + *vertical_angle_end = dmic->dmic_mic_config.vendor[index].vertical_angle_end; + *horizontal_angle_begin = dmic->dmic_mic_config.vendor[index].horizontal_angle_begin; + *horizontal_angle_end = dmic->dmic_mic_config.vendor[index].horizontal_angle_end; + + return 0; +} + +int dmic_get_vendor_blob_size(struct intel_nhlt_params *nhlt, size_t *size) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + int i, fir_index_0, fir_index_1; + + if (!dmic || !dmic->dmic_count) + return -EINVAL; + + *size = sizeof(struct dmic_intel_config_data); + + /* if either of the fir is 0 length, copy the existing fir twice */ + fir_index_0 = 0; + fir_index_1 = 1; + if (dmic->dmic_fir_array.fir_len[0] == 0) { + fir_index_0 = 1; + fir_index_1 = 1; + } + if (dmic->dmic_fir_array.fir_len[1] == 0) { + fir_index_0 = 0; + fir_index_1 = 0; + } + + /* variable amount of pdm's */ + for (i = 0; i < DMIC_HW_CONTROLLERS; i++) { + /* only copy the pdm data if it is enabled */ + if ((dmic->dmic_blob.channel_pdm_mask & BIT(i)) == 0) + continue; + + *size += sizeof(struct dmic_intel_pdm_ctrl_cfg); + *size += sizeof(struct dmic_intel_fir_config) * DMIC_HW_FIFOS; + + *size += dmic->dmic_fir_array.fir_len[fir_index_0] * sizeof(uint32_t); + *size += dmic->dmic_fir_array.fir_len[fir_index_1] * sizeof(uint32_t); + } + + return 0; +} + +int dmic_get_vendor_blob_count(struct intel_nhlt_params *nhlt) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + + if (!dmic || !dmic->dmic_count) + return 0; + + return dmic->dmic_count; +} + +int dmic_get_vendor_blob(struct intel_nhlt_params *nhlt, uint8_t *vendor_blob) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + int i, fir_index_0, fir_index_1; + uint8_t *orig_blob = vendor_blob; + size_t blob_size; + + if (!dmic || !dmic->dmic_count) + return -EINVAL; + + /* top level struct */ + memcpy(vendor_blob, &dmic->dmic_blob, sizeof(struct dmic_intel_config_data)); + vendor_blob += sizeof(struct dmic_intel_config_data); + + /* if either of the fir is 0 length, copy the existing fir twice */ + fir_index_0 = 0; + fir_index_1 = 1; + if (dmic->dmic_fir_array.fir_len[0] == 0) { + fir_index_0 = 1; + fir_index_1 = 1; + } + if (dmic->dmic_fir_array.fir_len[1] == 0) { + fir_index_0 = 0; + fir_index_1 = 0; + } + + /* variable amount of pdm's */ + for (i = 0; i < DMIC_HW_CONTROLLERS; i++) { + /* only copy the pdm data if it is enabled */ + if ((dmic->dmic_blob.channel_pdm_mask & BIT(i)) == 0) + continue; + + /* top level struct first pdm data */ + memcpy(vendor_blob, (uint8_t *)&dmic->dmic_blob_pdm[i], + sizeof(struct dmic_intel_pdm_ctrl_cfg)); + vendor_blob += sizeof(struct dmic_intel_pdm_ctrl_cfg); + + /* top level struct first pdm data first fir */ + memcpy(vendor_blob, (uint8_t *)&dmic->dmic_blob_fir[i][fir_index_0], + sizeof(struct dmic_intel_fir_config)); + vendor_blob += sizeof(struct dmic_intel_fir_config); + + /* top level struct first pdm data second fir */ + memcpy(vendor_blob, (uint8_t *)&dmic->dmic_blob_fir[i][fir_index_1], + sizeof(struct dmic_intel_fir_config)); + vendor_blob += sizeof(struct dmic_intel_fir_config); + + /* fir coeffs a */ + memcpy(vendor_blob, (uint8_t *)&dmic->dmic_fir_array.fir_coeffs[i][fir_index_0][0], + dmic->dmic_fir_array.fir_len[fir_index_0] * sizeof(uint32_t)); + vendor_blob += dmic->dmic_fir_array.fir_len[fir_index_0] * sizeof(uint32_t); + + /* fir coeffs b */ + memcpy(vendor_blob, (uint8_t *)&dmic->dmic_fir_array.fir_coeffs[i][fir_index_1][0], + dmic->dmic_fir_array.fir_len[fir_index_1] * sizeof(uint32_t)); + vendor_blob += dmic->dmic_fir_array.fir_len[fir_index_1] * sizeof(uint32_t); + } + + dmic_get_vendor_blob_size(nhlt, &blob_size); + dmic_print_bytes_as_hex((uint8_t *)orig_blob, blob_size); + dmic_print_integers_as_hex((uint32_t *)orig_blob, blob_size / 4); + + return 0; +} + +int dmic_set_params(struct intel_nhlt_params *nhlt, int dai_index, int driver_version, + int io_clk, int num_pdm_active, int fifo_word_length, int clk_min, int clk_max, + int duty_min, int duty_max, int sample_rate, int unmute_ramp_time) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + + if (!dmic) + return -EINVAL; + + if (dai_index >= DMIC_HW_FIFOS) { + fprintf(stderr, "%s: illegal dai index %d \n", __func__, dai_index); + return -EINVAL; + } + + if (driver_version < 1 || driver_version > 5) { + fprintf(stderr, "%s: illegal driver version %d\n", __func__, driver_version); + return -EINVAL; + } + + dmic->dmic_dai_index = dai_index; + dmic->dmic_prm[dai_index].driver_version = driver_version; + dmic->dmic_prm[dai_index].io_clk = io_clk; + dmic->dmic_prm[dai_index].num_pdm_active = num_pdm_active; + dmic->dmic_prm[dai_index].fifo_bits = fifo_word_length; + dmic->dmic_prm[dai_index].pdmclk_min = clk_min; + dmic->dmic_prm[dai_index].pdmclk_max = clk_max; + dmic->dmic_prm[dai_index].duty_min = duty_min; + dmic->dmic_prm[dai_index].duty_max = duty_max; + dmic->dmic_prm[dai_index].fifo_fs = sample_rate; + dmic->dmic_prm[dai_index].unmute_ramp_time = unmute_ramp_time; + + return 0; +} + +int dmic_set_pdm_params(struct intel_nhlt_params *nhlt, int pdm_index, int enable_a, + int enable_b, int polarity_a, int polarity_b, int clk_edge, int skew) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + int di; + + if (!dmic) + return -EINVAL; + + if (pdm_index >= DMIC_HW_CONTROLLERS) { + fprintf(stderr, "%s: illegal pdm_index %d\n", __func__, pdm_index); + return -EINVAL; + } + + di = dmic->dmic_dai_index; + + dmic->dmic_prm[di].pdm[pdm_index].enable_mic_a = enable_a; + dmic->dmic_prm[di].pdm[pdm_index].enable_mic_b = enable_b; + dmic->dmic_prm[di].pdm[pdm_index].polarity_mic_a = polarity_a; + dmic->dmic_prm[di].pdm[pdm_index].polarity_mic_b = polarity_b; + dmic->dmic_prm[di].pdm[pdm_index].clk_edge = clk_edge; + dmic->dmic_prm[di].pdm[pdm_index].skew = skew; + + return 0; +} + +int dmic_set_ext_params(struct intel_nhlt_params *nhlt, uint32_t snr, uint32_t sensitivity) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + + if (!dmic) + return -EINVAL; + + dmic->dmic_mic_config.extension = 1; + dmic->dmic_mic_config.snr = snr; + dmic->dmic_mic_config.sensitivity = sensitivity; + + return 0; +} + +int dmic_set_mic_params(struct intel_nhlt_params *nhlt, int index, + uint8_t type, uint8_t panel, uint32_t speaker_position_distance, + uint32_t horizontal_offset, uint32_t vertical_offset, + uint8_t frequency_low_band, uint8_t frequency_high_band, + uint16_t direction_angle, uint16_t elevation_angle, + uint16_t vertical_angle_begin, uint16_t vertical_angle_end, + uint16_t horizontal_angle_begin, uint16_t horizontal_angle_end) +{ + struct intel_dmic_params *dmic = (struct intel_dmic_params *)nhlt->dmic_params; + + if (!dmic) + return -EINVAL; + + dmic->dmic_mic_config.vendor[index].type = type; + dmic->dmic_mic_config.vendor[index].panel = panel; + dmic->dmic_mic_config.vendor[index].speaker_position_distance = speaker_position_distance; + dmic->dmic_mic_config.vendor[index].horizontal_offset = horizontal_offset; + dmic->dmic_mic_config.vendor[index].vertical_offset = vertical_offset; + dmic->dmic_mic_config.vendor[index].frequency_low_band = frequency_low_band; + dmic->dmic_mic_config.vendor[index].frequency_high_band = frequency_high_band; + dmic->dmic_mic_config.vendor[index].direction_angle = direction_angle; + dmic->dmic_mic_config.vendor[index].elevation_angle = elevation_angle; + dmic->dmic_mic_config.vendor[index].vertical_angle_begin = vertical_angle_begin; + dmic->dmic_mic_config.vendor[index].vertical_angle_end = vertical_angle_end; + dmic->dmic_mic_config.vendor[index].horizontal_angle_begin = horizontal_angle_begin; + dmic->dmic_mic_config.vendor[index].horizontal_angle_end = horizontal_angle_end; + + dmic->dmic_mic_config.num_mics++; + + return 0; +} + +/* init dmic parameters, should be called before parsing dais */ +int dmic_init_params(struct intel_nhlt_params *nhlt) +{ + struct intel_dmic_params *dmic; + int i; + + dmic = calloc(1, sizeof(struct intel_dmic_params)); + if (!dmic) + return -ENOMEM; + + nhlt->dmic_params = dmic; + /* set always to 1, some fw variants use this for choosing memory type */ + dmic->dmic_blob.gateway_attributes = 1; + /* delay in ms to unmute mics after clock is started */ + dmic->dmic_blob.clock_on_delay = 16; + + for (i = 0; i < DMIC_TS_GROUP_SIZE; i++) + dmic->dmic_blob.ts_group[i] = 0xFFFFFFFF; /* not enabled */ + + dmic->dmic_count = 0; + + dmic->dmic_mic_config.num_mics = 0; + dmic->dmic_mic_config.extension = 0; + dmic->dmic_mic_config.array_type = 0; + dmic->dmic_mic_config.snr = 0; + dmic->dmic_mic_config.sensitivity = 0; + + return 0; +} diff --git a/topology/nhlt/intel/dmic/dmic-process.h b/topology/nhlt/intel/dmic/dmic-process.h new file mode 100644 index 0000000..e67f473 --- /dev/null +++ b/topology/nhlt/intel/dmic/dmic-process.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#ifndef __DMIC_PROCESS_H +#define __DMIC_PROCESS_H + +#include + +/* initialize and set default values before parsing */ +int dmic_init_params(struct intel_nhlt_params *nhlt); + +/* set parameters when parsing topology2 conf */ +int dmic_set_params(struct intel_nhlt_params *nhlt, int dai_index, int driver_version, + int io_clk, int num_pdm_active, int fifo_word_length, int clk_min, int clk_max, + int duty_min, int duty_max, int sample_rate, int unmute_ramp_time); +int dmic_set_pdm_params(struct intel_nhlt_params *nhlt, int pdm_index, int enable_a, + int enable_b, int polarity_a, int polarity_b, int clk_edge, int skew); +int dmic_set_ext_params(struct intel_nhlt_params *nhlt, uint32_t snr, uint32_t sensitivity); +int dmic_set_mic_params(struct intel_nhlt_params *nhlt, int index, + uint8_t type, uint8_t panel, uint32_t speaker_position_distance, + uint32_t horizontal_offset, uint32_t vertical_offset, + uint8_t frequency_low_band, uint8_t frequency_high_band, + uint16_t direction_angle, uint16_t elevation_angle, + uint16_t vertical_angle_begin, uint16_t vertical_angle_end, + uint16_t horizontal_angle_begin, uint16_t horizontal_angle_end); + +/* calculate the blob after parsing the values*/ +int dmic_calculate(struct intel_nhlt_params *nhlt); + +/* get spec parameters when building the nhlt endpoint */ +int dmic_get_params(struct intel_nhlt_params *nhlt, int index, uint32_t *sample_rate, + uint16_t *channel_count, uint32_t *bits_per_sample, uint8_t *array_type, + uint8_t *num_mics, uint8_t *extension, uint32_t *snr, uint32_t *sensitivity); +int dmic_get_mic_params(struct intel_nhlt_params *nhlt, int index, + uint8_t *type, uint8_t *panel, uint32_t *speaker_position_distance, + uint32_t *horizontal_offset, uint32_t *vertical_offset, + uint8_t *frequency_low_band, uint8_t *frequency_high_band, + uint16_t *direction_angle, uint16_t *elevation_angle, + uint16_t *vertical_angle_begin, uint16_t *vertical_angle_end, + uint16_t *horizontal_angle_begin, uint16_t *horizontal_angle_end); + +/* get vendor specific blob when building the nhlt endpoint */ +int dmic_get_vendor_blob_count(struct intel_nhlt_params *nhlt); +int dmic_get_vendor_blob_size(struct intel_nhlt_params *nhlt, size_t *size); +int dmic_get_vendor_blob(struct intel_nhlt_params *nhlt, uint8_t *vendor_blob); + +#endif diff --git a/topology/nhlt/intel/dmic/pdm-decim-fir.h b/topology/nhlt/intel/dmic/pdm-decim-fir.h new file mode 100644 index 0000000..7f165ae --- /dev/null +++ b/topology/nhlt/intel/dmic/pdm-decim-fir.h @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021-2023 Intel Corporation. All rights reserved. +// +// Author: Seppo Ingalsuo +// Jaska Uimonen + +#ifndef __SOF_AUDIO_COEFFICIENTS_PDM_DECIM_PDM_DECIM_FIR_H__ +#define __SOF_AUDIO_COEFFICIENTS_PDM_DECIM_PDM_DECIM_FIR_H__ + +#include + +struct pdm_decim { + int decim_factor; + int length; + int shift; + int relative_passband; + int relative_stopband; + int passband_ripple; + int stopband_ripple; + const int32_t *coef; +}; + +const int32_t fir_int32_10_4156_5345_010_090[250] = { + 1523665, 1033186, 1237912, 1334775, 1259136, + 945771, 330804, -639430, -2007230, -3782603, + -5951062, -8464725, -11233139, -14135396, -17013193, + -19685639, -21953913, -23620153, -24499971, -24438406, + -23328862, -21124651, -17851431, -13614282, -8597813, + -3061758, 2673313, 8241850, 13260214, 17352951, + 20183225, 21483036, 21079871, 18918865, 15076270, + 9764507, 3325641, -3786831, -11034844, -17834361, + -23598518, -27786005, -29948197, -29773031, -27120493, + -22045821, -14809184, -5868041, 4147420, 14474606, + 24273606, 32691772, 38934943, 42337380, 42426833, + 38977193, 32043932, 21978645, 9419218, -4744590, + -19429341, -33436251, -45544284, -54611932, -59678078, + -60055175, -55404949, -45789928, -31694490, -14011800, + 6004287, 26821132, 46737621, 64015881, 77024918, + 84384267, 85096503, 78655977, 65124057, 45161629, + 20014065, -8553289, -38367257, -66999907, -91952963, + -110860336, -121693302, -122951962, -113826153, -94311463, + -65267325, -28409399, 13767679, 58135590, 101138413, + 139047421, 168247614, 185536046, 188409622, 175319624, + 145872050, 100954938, 42779310, -25174601, -98307054, + -171138928, -237629824, -291555223, -326919157, -338374861, + -321624361, -273768560, -193581379, -81686484, 59379271, + 225223665, 409778420, 605591661, 804217132, 996677146, + 1173970423, 1327592623, 1450036054, 1535235882, 1578933611, + 1578933611, 1535235882, 1450036054, 1327592623, 1173970423, + 996677146, 804217132, 605591661, 409778420, 225223665, + 59379271, -81686484, -193581379, -273768560, -321624361, + -338374861, -326919157, -291555223, -237629824, -171138928, + -98307054, -25174601, 42779310, 100954938, 145872050, + 175319624, 188409622, 185536046, 168247614, 139047421, + 101138413, 58135590, 13767679, -28409399, -65267325, + -94311463, -113826153, -122951962, -121693302, -110860336, + -91952963, -66999907, -38367257, -8553289, 20014065, + 45161629, 65124057, 78655977, 85096503, 84384267, + 77024918, 64015881, 46737621, 26821132, 6004287, + -14011800, -31694490, -45789928, -55404949, -60055175, + -59678078, -54611932, -45544284, -33436251, -19429341, + -4744590, 9419218, 21978645, 32043932, 38977193, + 42426833, 42337380, 38934943, 32691772, 24273606, + 14474606, 4147420, -5868041, -14809184, -22045821, + -27120493, -29773031, -29948197, -27786005, -23598518, + -17834361, -11034844, -3786831, 3325641, 9764507, + 15076270, 18918865, 21079871, 21483036, 20183225, + 17352951, 13260214, 8241850, 2673313, -3061758, + -8597813, -13614282, -17851431, -21124651, -23328862, + -24438406, -24499971, -23620153, -21953913, -19685639, + -17013193, -14135396, -11233139, -8464725, -5951062, + -3782603, -2007230, -639430, 330804, 945771, + 1259136, 1334775, 1237912, 1033186, 1523665 +}; + +struct pdm_decim pdm_decim_int32_10_4156_5345_010_090 = { + 10, 250, 3, 4156, 5345, 10, 90, fir_int32_10_4156_5345_010_090 +}; + +const int32_t fir_int32_05_4325_5100_010_095[249] = { + -207469, -340409, -498144, -558705, -409384, + 55040, 891125, 2067198, 3439775, 4759611, + 5714914, 6006588, 5438767, 3999938, 1904139, + -429386, -2461501, -3672469, -3715731, -2543409, + -455850, 1949813, 3931230, 4816572, 4223058, + 2208032, -705100, -3649781, -5664341, -5997954, + -4379534, -1155856, 2760627, 6138124, 7803235, + 7038238, 3865643, -886573, -5783539, -9205487, + -9873081, -7309023, -2074085, 4324112, 9849851, + 12575185, 11338304, 6203771, -1449001, -9273272, + -14659745, -15592551, -11379919, -3001447, 7079011, + 15617750, 19618936, 17365380, 9103870, -2875883, + -14845719, -22779047, -23684744, -16699479, -3549687, + 11824128, 24420688, 29781202, 25536278, 12369331, + -5975020, -23715313, -34825066, -35046780, -23465500, + -3076561, 19921841, 37971240, 44596949, 36675890, + 15750350, -12128842, -38090537, -53213360, -51529430, + -32269554, -535756, 33948414, 59741809, 67400482, + 52822241, 19070165, -24058950, -62761384, -83526757, + -77707237, -44854657, 6391470, 60372631, 99010031, + 107635426, 80237884, 22341840, -49653145, -112793869, + -144502927, -130123093, -68698546, 25049444, 123586766, + 193887099, 207405271, 149914650, 28201854, -128717667, + -274782040, -357589632, -332094228, -173714910, 112793207, + 491412574, 900833942, 1267184732, 1520519220, 1610979079, + 1520519220, 1267184732, 900833942, 491412574, 112793207, + -173714910, -332094228, -357589632, -274782040, -128717667, + 28201854, 149914650, 207405271, 193887099, 123586766, + 25049444, -68698546, -130123093, -144502927, -112793869, + -49653145, 22341840, 80237884, 107635426, 99010031, + 60372631, 6391470, -44854657, -77707237, -83526757, + -62761384, -24058950, 19070165, 52822241, 67400482, + 59741809, 33948414, -535756, -32269554, -51529430, + -53213360, -38090537, -12128842, 15750350, 36675890, + 44596949, 37971240, 19921841, -3076561, -23465500, + -35046780, -34825066, -23715313, -5975020, 12369331, + 25536278, 29781202, 24420688, 11824128, -3549687, + -16699479, -23684744, -22779047, -14845719, -2875883, + 9103870, 17365380, 19618936, 15617750, 7079011, + -3001447, -11379919, -15592551, -14659745, -9273272, + -1449001, 6203771, 11338304, 12575185, 9849851, + 4324112, -2074085, -7309023, -9873081, -9205487, + -5783539, -886573, 3865643, 7038238, 7803235, + 6138124, 2760627, -1155856, -4379534, -5997954, + -5664341, -3649781, -705100, 2208032, 4223058, + 4816572, 3931230, 1949813, -455850, -2543409, + -3715731, -3672469, -2461501, -429386, 1904139, + 3999938, 5438767, 6006588, 5714914, 4759611, + 3439775, 2067198, 891125, 55040, -409384, + -558705, -498144, -340409, -207469 +}; + +struct pdm_decim pdm_decim_int32_05_4325_5100_010_095 = { + 5, 249, 2, 4325, 5100, 10, 95, fir_int32_05_4325_5100_010_095 +}; + +const int32_t fir_int32_12_4156_5345_010_090[250] = { + 3388064, 2103678, 2588621, 3003201, 3289311, + 3375616, 3187681, 2654590, 1715972, 309000, + -1610211, -4055571, -7035886, -10511174, -14426105, + -18683424, -23158811, -27697959, -32125744, -36244313, + -39850668, -42739502, -44716782, -45614286, -45293362, + -43663492, -40685637, -36382432, -30840569, -24214188, + -16719141, -8631481, -273616, 7996830, 15802429, + 22766807, 28531588, 32779134, 35250465, 35763733, + 34227329, 30653000, 25158397, 17970176, 9416017, + -86479, -10047026, -19926252, -29162149, -37200047, + -43523733, -47686853, -49340539, -48260145, -44362718, + -37720992, -28567244, -17289156, -4416600, 9400589, + 23423041, 36858656, 48904650, 58793401, 65836995, + 69472264, 69298737, 65111461, 56923398, 44977350, + 29744904, 11913285, -7641844, -27894994, -47720356, + -65950400, -81438916, -93127908, -100112283, -101700449, + -97465921, -87287745, -71375413, -50277965, -24873838, + 3657557, 33882414, 64176403, 92802029, 117997525, + 138071930, 151502587, 157029100, 153740023, 141145505, + 119233461, 88503690, 49978361, 5186664, -43876910, + -94819563, -144940463, -191340721, -231048840, -261155504, + -278952672, -282068987, -268596392, -237200378, -187208906, + -118674579, -32406655, 70029801, 186348178, 313607557, + 448310730, 586528751, 724047761, 856530828, 979688864, + 1089452098, 1182135271, 1254588364, 1304326765, 1329634160, + 1329634160, 1304326765, 1254588364, 1182135271, 1089452098, + 979688864, 856530828, 724047761, 586528751, 448310730, + 313607557, 186348178, 70029801, -32406655, -118674579, + -187208906, -237200378, -268596392, -282068987, -278952672, + -261155504, -231048840, -191340721, -144940463, -94819563, + -43876910, 5186664, 49978361, 88503690, 119233461, + 141145505, 153740023, 157029100, 151502587, 138071930, + 117997525, 92802029, 64176403, 33882414, 3657557, + -24873838, -50277965, -71375413, -87287745, -97465921, + -101700449, -100112283, -93127908, -81438916, -65950400, + -47720356, -27894994, -7641844, 11913285, 29744904, + 44977350, 56923398, 65111461, 69298737, 69472264, + 65836995, 58793401, 48904650, 36858656, 23423041, + 9400589, -4416600, -17289156, -28567244, -37720992, + -44362718, -48260145, -49340539, -47686853, -43523733, + -37200047, -29162149, -19926252, -10047026, -86479, + 9416017, 17970176, 25158397, 30653000, 34227329, + 35763733, 35250465, 32779134, 28531588, 22766807, + 15802429, 7996830, -273616, -8631481, -16719141, + -24214188, -30840569, -36382432, -40685637, -43663492, + -45293362, -45614286, -44716782, -42739502, -39850668, + -36244313, -32125744, -27697959, -23158811, -18683424, + -14426105, -10511174, -7035886, -4055571, -1610211, + 309000, 1715972, 2654590, 3187681, 3375616, + 3289311, 3003201, 2588621, 2103678, 3388064 +}; + +struct pdm_decim pdm_decim_int32_12_4156_5345_010_090 = { + 12, 250, 3, 4156, 5345, 10, 90, fir_int32_12_4156_5345_010_090 +}; + +const int32_t fir_int32_03_4375_5100_010_095[157] = { + 350908, 1127906, 2233575, 3059598, 2752218, + 818077, -2252677, -4944563, -5550761, -3466262, + 53093, 2496787, 1904133, -1421749, -4818616, + -5204506, -1721103, 3155344, 5311508, 2454543, + -3518663, -7589567, -5713379, 1327838, 7901439, + 7958184, 527907, -8634105, -11354937, -4214569, + 7627213, 13970417, 8263468, -5862019, -16549858, + -13530131, 2213952, 17870989, 19056458, 2854116, + -18029944, -24979416, -9860304, 16175288, 30546328, + 18606151, -11894439, -35172976, -28918520, 4746588, + 38201563, 40591159, 5825487, -38713444, -53159813, + -20283635, 35723701, 66121349, 39266346, -27911327, + -78796917, -63664545, 13459132, 90417174, 95194527, + 10755265, -99898306, -137498952, -51076476, 105458775, + 200050300, 124562550, -101613472, -313388303, -297347454, + 53702567, 639689683, 1187815441, 1411068556, 1187815441, + 639689683, 53702567, -297347454, -313388303, -101613472, + 124562550, 200050300, 105458775, -51076476, -137498952, + -99898306, 10755265, 95194527, 90417174, 13459132, + -63664545, -78796917, -27911327, 39266346, 66121349, + 35723701, -20283635, -53159813, -38713444, 5825487, + 40591159, 38201563, 4746588, -28918520, -35172976, + -11894439, 18606151, 30546328, 16175288, -9860304, + -24979416, -18029944, 2854116, 19056458, 17870989, + 2213952, -13530131, -16549858, -5862019, 8263468, + 13970417, 7627213, -4214569, -11354937, -8634105, + 527907, 7958184, 7901439, 1327838, -5713379, + -7589567, -3518663, 2454543, 5311508, 3155344, + -1721103, -5204506, -4818616, -1421749, 1904133, + 2496787, 53093, -3466262, -5550761, -4944563, + -2252677, 818077, 2752218, 3059598, 2233575, + 1127906, 350908 +}; + +struct pdm_decim pdm_decim_int32_03_4375_5100_010_095 = { + 3, 157, 1, 4375, 5100, 10, 95, fir_int32_03_4375_5100_010_095 +}; + +const int32_t fir_int32_02_4375_5100_010_095[101] = { + -587830, -2653881, -5154608, -4845367, -226474, + 4220832, 2571159, -3184700, -4043579, 2206821, + 5554546, -750495, -6923897, -1268580, 8073364, + 4085184, -8546479, -7505366, 8176184, 11533751, + -6471060, -15704257, 3359705, 19852407, 1635592, + -23144509, -8252640, 25285011, 16574477, -25723227, + -26663303, 23549736, 38139662, -17943368, -50446982, + 8141045, 63090266, 7051348, -75166961, -29039893, + 85772628, 60568976, -93167361, -106799777, 94198977, + 180962817, -78385599, -324820256, -12243140, 742491464, + 1151461314, 742491464, -12243140, -324820256, -78385599, + 180962817, 94198977, -106799777, -93167361, 60568976, + 85772628, -29039893, -75166961, 7051348, 63090266, + 8141045, -50446982, -17943368, 38139662, 23549736, + -26663303, -25723227, 16574477, 25285011, -8252640, + -23144509, 1635592, 19852407, 3359705, -15704257, + -6471060, 11533751, 8176184, -7505366, -8546479, + 4085184, 8073364, -1268580, -6923897, -750495, + 5554546, 2206821, -4043579, -3184700, 2571159, + 4220832, -226474, -4845367, -5154608, -2653881, + -587830 +}; + +struct pdm_decim pdm_decim_int32_02_4375_5100_010_095 = { + 2, 101, 0, 4375, 5100, 10, 95, fir_int32_02_4375_5100_010_095 +}; + +const int32_t fir_int32_08_4156_5301_010_090[249] = { + -436533, -30097, 185136, 599151, 1249127, + 2156309, 3316125, 4690126, 6201703, 7736149, + 9146691, 10266194, 10924643, 10970009, 10291237, + 8839410, 6645091, 3827796, 595625, -2767301, + -5925074, -8524488, -10235869, -10797193, -10055131, + -7997758, -4771108, -678235, 3841448, 8252025, + 11985305, 14503017, 15375105, 14342998, 11370408, + 6669744, 697166, -5883789, -12270613, -17614188, + -21126754, -22191034, -20457484, -15913860, -8916788, + -176408, 9306801, 18349873, 25728753, 30337331, + 31343834, 28324134, 21351665, 11028693, -1551253, + -14908738, -27343830, -37144407, -42813431, -43283070, + -38091954, -27492154, -12471488, 5321162, 23744416, + 40410795, 52985294, 59498920, 58637630, 49965561, + 34048834, 12455105, -12379340, -37407904, -59331472, + -75016381, -81921345, -78477412, -64367569, -40661743, + -9779029, 24730505, 58569248, 87201632, 106433026, + 112987295, 105007254, 82406938, 47020690, 2512109, + -45960354, -92298012, -130180961, -153871591, -159007197, + -143278921, -106903512, -52811885, 13489989, 84396518, + 150980417, 204001696, 235041565, 237634873, 208265018, + 147093489, 58319704, -49897266, -165975625, -275876513, + -364417203, -416806765, -420248734, -365439450, -247794421, + -68258347, 166406038, 443903891, 747260153, 1056170460, + 1348727100, 1603354585, 1800763975, 1925728602, 1968501522, + 1925728602, 1800763975, 1603354585, 1348727100, 1056170460, + 747260153, 443903891, 166406038, -68258347, -247794421, + -365439450, -420248734, -416806765, -364417203, -275876513, + -165975625, -49897266, 58319704, 147093489, 208265018, + 237634873, 235041565, 204001696, 150980417, 84396518, + 13489989, -52811885, -106903512, -143278921, -159007197, + -153871591, -130180961, -92298012, -45960354, 2512109, + 47020690, 82406938, 105007254, 112987295, 106433026, + 87201632, 58569248, 24730505, -9779029, -40661743, + -64367569, -78477412, -81921345, -75016381, -59331472, + -37407904, -12379340, 12455105, 34048834, 49965561, + 58637630, 59498920, 52985294, 40410795, 23744416, + 5321162, -12471488, -27492154, -38091954, -43283070, + -42813431, -37144407, -27343830, -14908738, -1551253, + 11028693, 21351665, 28324134, 31343834, 30337331, + 25728753, 18349873, 9306801, -176408, -8916788, + -15913860, -20457484, -22191034, -21126754, -17614188, + -12270613, -5883789, 697166, 6669744, 11370408, + 14342998, 15375105, 14503017, 11985305, 8252025, + 3841448, -678235, -4771108, -7997758, -10055131, + -10797193, -10235869, -8524488, -5925074, -2767301, + 595625, 3827796, 6645091, 8839410, 10291237, + 10970009, 10924643, 10266194, 9146691, 7736149, + 6201703, 4690126, 3316125, 2156309, 1249127, + 599151, 185136, -30097, -436533 +}; + +struct pdm_decim pdm_decim_int32_08_4156_5301_010_090 = { + 8, 249, 3, 4156, 5301, 10, 90, fir_int32_08_4156_5301_010_090 +}; + +const int32_t fir_int32_02_4323_5100_010_095[95] = { + 178179, -158757, -2195582, -5296650, -5737416, + -1057695, 4405719, 3336648, -3249588, -5061179, + 1984305, 6895125, 68826, -8433396, -2933479, + 9499107, 6882087, -9330152, -11397510, 7807097, + 16376076, -4402338, -21239788, -1118085, 25423993, + 9062534, -27935015, -19203927, 28049586, 31500423, + -24524863, -45191501, 16582731, 59861920, -2808306, + -74639091, -18696113, 88054673, 50505898, -98266320, + -97865783, 101816481, 173879965, -88042495, -320187025, + -1193013, 740698712, 1139586920, 740698712, -1193013, + -320187025, -88042495, 173879965, 101816481, -97865783, + -98266320, 50505898, 88054673, -18696113, -74639091, + -2808306, 59861920, 16582731, -45191501, -24524863, + 31500423, 28049586, -19203927, -27935015, 9062534, + 25423993, -1118085, -21239788, -4402338, 16376076, + 7807097, -11397510, -9330152, 6882087, 9499107, + -2933479, -8433396, 68826, 6895125, 1984305, + -5061179, -3249588, 3336648, 4405719, -1057695, + -5737416, -5296650, -2195582, -158757, 178179 +}; + +struct pdm_decim pdm_decim_int32_02_4323_5100_010_095 = { + 2, 95, 0, 4323, 5100, 10, 95, fir_int32_02_4323_5100_010_095 +}; + +const int32_t fir_int32_06_4172_5100_010_095[247] = { + -128631, -59503, 27031, 238532, 615330, + 1180314, 1925562, 2802417, 3717924, 4540883, + 5118361, 5301921, 4979167, 4105027, 2725204, + 985420, -879357, -2576519, -3805386, -4316122, + -3967965, -2772550, -910853, 1284237, 3378600, + 4918468, 5523902, 4978553, 3294183, 731440, + -2230675, -4978319, -6884321, -7446349, -6411064, + -3855969, -204877, 3832625, 7392494, 9635125, + 9936780, 8050664, 4198358, -936188, -6317705, + -10761140, -13185684, -12868530, -9640702, -3974823, + 3065204, 10012726, 15295833, 17583895, 16110094, + 10893000, 2799002, -6583053, -15241642, -21163671, + -22790698, -19409646, -11383158, -154530, 11992615, + 22384784, 28533872, 28715941, 22422705, 10572525, + -4585611, -19877670, -31851611, -37538237, -35161448, + -24633854, -7707813, 12278124, 31031785, 44204723, + 48372136, 41880203, 25359325, 1761603, -24122660, + -46601256, -60293649, -61370947, -48545937, -23571586, + 8902814, 42136842, 68639354, 81788695, 77395612, + 54857291, 17613243, -27256826, -70277333, -101463585, + -112494642, -98725143, -60586317, -4019284, 60240464, + 118482133, 156662559, 163400089, 132736532, 66085166, + -27098212, -130164932, -221605671, -278535815, -280760534, + -214700503, -76443585, 126687932, 376420234, 645703534, + 902480880, 1114453256, 1254049394, 1302754294, 1254049394, + 1114453256, 902480880, 645703534, 376420234, 126687932, + -76443585, -214700503, -280760534, -278535815, -221605671, + -130164932, -27098212, 66085166, 132736532, 163400089, + 156662559, 118482133, 60240464, -4019284, -60586317, + -98725143, -112494642, -101463585, -70277333, -27256826, + 17613243, 54857291, 77395612, 81788695, 68639354, + 42136842, 8902814, -23571586, -48545937, -61370947, + -60293649, -46601256, -24122660, 1761603, 25359325, + 41880203, 48372136, 44204723, 31031785, 12278124, + -7707813, -24633854, -35161448, -37538237, -31851611, + -19877670, -4585611, 10572525, 22422705, 28715941, + 28533872, 22384784, 11992615, -154530, -11383158, + -19409646, -22790698, -21163671, -15241642, -6583053, + 2799002, 10893000, 16110094, 17583895, 15295833, + 10012726, 3065204, -3974823, -9640702, -12868530, + -13185684, -10761140, -6317705, -936188, 4198358, + 8050664, 9936780, 9635125, 7392494, 3832625, + -204877, -3855969, -6411064, -7446349, -6884321, + -4978319, -2230675, 731440, 3294183, 4978553, + 5523902, 4918468, 3378600, 1284237, -910853, + -2772550, -3967965, -4316122, -3805386, -2576519, + -879357, 985420, 2725204, 4105027, 4979167, + 5301921, 5118361, 4540883, 3717924, 2802417, + 1925562, 1180314, 615330, 238532, 27031, + -59503, -128631 +}; + +struct pdm_decim pdm_decim_int32_06_4172_5100_010_095 = { + 6, 247, 2, 4172, 5100, 10, 95, fir_int32_06_4172_5100_010_095 +}; + +const int32_t fir_int32_04_4318_5100_010_095[195] = { + 111466, 733409, 1749250, 3319696, 5129378, + 6676209, 7309490, 6506584, 4154756, 734975, + -2729377, -4998637, -5126868, -2945573, 726080, + 4306371, 6084832, 5022201, 1343898, -3405897, + -6962146, -7384707, -4083477, 1706504, 7322247, + 9858386, 7634375, 1218279, -6584983, -11909667, + -11705999, -5426216, 4387589, 13057254, 15953274, + 10914694, -336708, -12649310, -19740590, -17350504, + -5691816, 10168034, 22423979, 24315225, 13770305, + -5012695, -23099234, -30992981, -23541145, -3074594, + 21044646, 36602258, 34627991, 14410179, -15304906, + -39942350, -46136506, -28837515, 5227124, 39904111, + 57125295, 46160603, 9951610, -35069112, -66221384, + -65802058, -30833475, 23892424, 71729303, 86873627, + 57925919, -4643620, -71630929, -108312985, -91957579, + -25024430, 63233146, 128873180, 134384678, 69003997, + -42277605, -146972769, -188542432, -135065835, 625004, + 160544910, 263771211, 243538822, 83417905, -164654723, + -391259178, -468017530, -299941690, 129616412, 741218294, + 1378462855, 1858765025, 2037199780, 1858765025, 1378462855, + 741218294, 129616412, -299941690, -468017530, -391259178, + -164654723, 83417905, 243538822, 263771211, 160544910, + 625004, -135065835, -188542432, -146972769, -42277605, + 69003997, 134384678, 128873180, 63233146, -25024430, + -91957579, -108312985, -71630929, -4643620, 57925919, + 86873627, 71729303, 23892424, -30833475, -65802058, + -66221384, -35069112, 9951610, 46160603, 57125295, + 39904111, 5227124, -28837515, -46136506, -39942350, + -15304906, 14410179, 34627991, 36602258, 21044646, + -3074594, -23541145, -30992981, -23099234, -5012695, + 13770305, 24315225, 22423979, 10168034, -5691816, + -17350504, -19740590, -12649310, -336708, 10914694, + 15953274, 13057254, 4387589, -5426216, -11705999, + -11909667, -6584983, 1218279, 7634375, 9858386, + 7322247, 1706504, -4083477, -7384707, -6962146, + -3405897, 1343898, 5022201, 6084832, 4306371, + 726080, -2945573, -5126868, -4998637, -2729377, + 734975, 4154756, 6506584, 7309490, 6676209, + 5129378, 3319696, 1749250, 733409, 111466 +}; + +struct pdm_decim pdm_decim_int32_04_4318_5100_010_095 = { + 4, 195, 2, 4318, 5100, 10, 95, fir_int32_04_4318_5100_010_095 +}; + +#endif /* __SOF_AUDIO_COEFFICIENTS_PDM_DECIM_PDM_DECIM_FIR_H__ */ diff --git a/topology/nhlt/intel/intel-nhlt.c b/topology/nhlt/intel/intel-nhlt.c new file mode 100644 index 0000000..b245afc --- /dev/null +++ b/topology/nhlt/intel/intel-nhlt.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#include "aconfig.h" +#include "intel-nhlt.h" + +static int get_int_val(snd_config_t *input, long *int_val, snd_config_t *top) +{ + char tplg_define[128] = "Define."; + snd_config_t *n; + const char *s; + int ret; + + if (snd_config_get_string(input, &s) < 0) + return snd_config_get_integer(input, int_val); + + if (*s != '$') + return 0; + + strcat(tplg_define, s + 1); + + ret = snd_config_search(top, tplg_define, &n); + if (ret < 0) + return ret; + + return snd_config_get_integer(n, int_val); +} + +static int get_string_val(snd_config_t *input, const char **string_val, snd_config_t *top) +{ + char tplg_define[128] = "Define."; + snd_config_t *n; + int ret; + + if (snd_config_get_string(input, string_val) < 0) + return -EINVAL; + + if (**string_val != '$') + return 0; + + strcat(tplg_define, *string_val + 1); + + ret = snd_config_search(top, tplg_define, &n); + if (ret < 0) + return ret; + + return snd_config_get_string(n, string_val); +} + +#ifdef NHLT_DEBUG +static void print_array_values(struct dai_values *values, int size) +{ + int i; + + fprintf(stdout, "print parsed array:\n"); + for (i = 0; i < size; i++, values++) { + if (values->type == SND_CONFIG_TYPE_INTEGER) + fprintf(stdout, "%s %ld\n", values->name, *values->int_val); + else + fprintf(stdout, "%s %s\n", values->name, *values->string_val); + } + fprintf(stdout, "\n"); +} +#endif + +int find_set_values(struct dai_values *values, int size, snd_config_t *dai_cfg, + snd_config_t *top, const char *class_name) +{ + snd_config_iterator_t i, next; + struct dai_values *temp_val; + snd_config_t *class_cfg; + snd_config_t *n; + const char *id; + int ret; + int j; + + /* get default values from class definition */ + ret = snd_config_search(top, class_name, &class_cfg); + if (ret < 0) + return ret; + + snd_config_for_each(i, next, class_cfg) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + for (j = 0, temp_val = values; j < size; j++, temp_val++) { + if (!strcmp(id, temp_val->name)) { + temp_val->data = n; + break; + } + } + } + + /* set instance specific values */ + snd_config_for_each(i, next, dai_cfg) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + for (j = 0, temp_val = values; j < size; j++, temp_val++) { + if (!strcmp(id, temp_val->name)) { + temp_val->data = n; + break; + } + } + } + + for (j = 0, temp_val = values; j < size; j++, temp_val++) { + if (!temp_val->data) + continue; + if (temp_val->type == SND_CONFIG_TYPE_INTEGER) + get_int_val(temp_val->data, temp_val->int_val, top); + else + get_string_val(temp_val->data, temp_val->string_val, top); + } + +#ifdef NHLT_DEBUG + print_array_values(values, size); +#endif + return 0; +} diff --git a/topology/nhlt/intel/intel-nhlt.h b/topology/nhlt/intel/intel-nhlt.h new file mode 100644 index 0000000..657b538 --- /dev/null +++ b/topology/nhlt/intel/intel-nhlt.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#ifndef __INTEL_NHLT_H +#define __INTEL_NHLT_H + +#include +#include +#include +#include +#include +#include + +#define MIN(a, b) ({ \ + typeof(a) __a = (a); \ + typeof(b) __b = (b); \ + __a > __b ? __b : __a; \ +}) +#define MAX(a, b) ({ \ + typeof(a) __a = (a); \ + typeof(b) __b = (b); \ + __a < __b ? __b : __a; \ +}) + +#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a)[0]) + +#define BIT(b) (1UL << (b)) +#define MASK(b_hi, b_lo) (((1ULL << ((b_hi) - (b_lo) + 1ULL)) - 1ULL) << (b_lo)) +#define SET_BIT(b, x) (((x) & 1) << (b)) +#define SET_BITS(b_hi, b_lo, x) (((x) & ((1ULL << ((b_hi) - (b_lo) + 1ULL)) - 1ULL)) << (b_lo)) + +struct intel_nhlt_params { + void *dmic_params; + void *ssp_params; +}; + +struct dai_values { + char name[32]; + snd_config_type_t type; + snd_config_t *data; + long *int_val; + const char **string_val; +}; + +int find_set_values(struct dai_values *values, int size, snd_config_t *dai_cfg, + snd_config_t *top, const char *class_name); + +#endif /* __INTEL_NHLT_H */ diff --git a/topology/nhlt/intel/ssp-nhlt.c b/topology/nhlt/intel/ssp-nhlt.c new file mode 100644 index 0000000..168e6bc --- /dev/null +++ b/topology/nhlt/intel/ssp-nhlt.c @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include "intel-nhlt.h" +#include "ssp-nhlt.h" +#include "ssp/ssp-process.h" +#include "ssp/ssp-internal.h" + +static int set_mn_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long m_div; + long n_div; + long ret; + + struct dai_values ssp_mn_data[] = { + {"m_div", SND_CONFIG_TYPE_INTEGER, NULL, &m_div, NULL}, + {"n_div", SND_CONFIG_TYPE_INTEGER, NULL, &n_div, NULL}, + }; + + ret = find_set_values(&ssp_mn_data[0], ARRAY_SIZE(ssp_mn_data), cfg, top, + "Class.Base.mn_config"); + if (ret < 0) + return ret; + + return ssp_mn_set_params(nhlt, m_div, n_div); +} + +static int set_clk_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long clock_warm_up; + long mclk; + long warm_up_ovr; + long clock_stop_delay; + long keep_running; + long clock_stop_ovr; + long ret; + + struct dai_values ssp_clk_data[] = { + {"clock_warm_up", SND_CONFIG_TYPE_INTEGER, NULL, &clock_warm_up, NULL}, + {"mclk", SND_CONFIG_TYPE_INTEGER, NULL, &mclk, NULL}, + {"warm_up_ovr", SND_CONFIG_TYPE_INTEGER, NULL, &warm_up_ovr, NULL}, + {"clock_stop_delay", SND_CONFIG_TYPE_INTEGER, NULL, &clock_stop_delay, NULL}, + {"keep_running", SND_CONFIG_TYPE_INTEGER, NULL, &keep_running, NULL}, + {"clock_stop_ovr", SND_CONFIG_TYPE_INTEGER, NULL, &clock_stop_ovr, NULL}, + }; + + ret = find_set_values(&ssp_clk_data[0], ARRAY_SIZE(ssp_clk_data), cfg, top, + "Class.Base.clk_config"); + if (ret < 0) + return ret; + + return ssp_clk_set_params(nhlt, clock_warm_up, mclk, warm_up_ovr, clock_stop_delay, + keep_running, clock_stop_ovr); +} + +static int set_tr_start_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long sampling_frequency; + long bit_depth; + long channel_map; + long channel_config; + long interleaving_style; + long number_of_channels; + long valid_bit_depth; + long sample_type; + long ret; + + struct dai_values ssp_tr_data[] = { + {"sampling_frequency", SND_CONFIG_TYPE_INTEGER, NULL, &sampling_frequency, NULL}, + {"bit_depth", SND_CONFIG_TYPE_INTEGER, NULL, &bit_depth, NULL}, + {"channel_map", SND_CONFIG_TYPE_INTEGER, NULL, &channel_map, NULL}, + {"channel_config", SND_CONFIG_TYPE_INTEGER, NULL, &channel_config, NULL}, + {"interleaving_style", SND_CONFIG_TYPE_INTEGER, NULL, &interleaving_style, NULL}, + {"number_of_channels", SND_CONFIG_TYPE_INTEGER, NULL, &number_of_channels, NULL}, + {"valid_bit_depth", SND_CONFIG_TYPE_INTEGER, NULL, &valid_bit_depth, NULL}, + {"sample_type", SND_CONFIG_TYPE_INTEGER, NULL, &sample_type, NULL}, + }; + + ret = find_set_values(&ssp_tr_data[0], ARRAY_SIZE(ssp_tr_data), cfg, top, + "Class.Base.tr_start_config"); + if (ret < 0) + return ret; + + return ssp_tr_start_set_params(nhlt, sampling_frequency, bit_depth, channel_map, + channel_config, interleaving_style, number_of_channels, + valid_bit_depth,sample_type); +} + +static int set_tr_stop_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long sampling_frequency; + long bit_depth; + long channel_map; + long channel_config; + long interleaving_style; + long number_of_channels; + long valid_bit_depth; + long sample_type; + long ret; + + struct dai_values ssp_tr_data[] = { + {"sampling_frequency", SND_CONFIG_TYPE_INTEGER, NULL, &sampling_frequency, NULL}, + {"bit_depth", SND_CONFIG_TYPE_INTEGER, NULL, &bit_depth, NULL}, + {"channel_map", SND_CONFIG_TYPE_INTEGER, NULL, &channel_map, NULL}, + {"channel_config", SND_CONFIG_TYPE_INTEGER, NULL, &channel_config, NULL}, + {"interleaving_style", SND_CONFIG_TYPE_INTEGER, NULL, &interleaving_style, NULL}, + {"number_of_channels", SND_CONFIG_TYPE_INTEGER, NULL, &number_of_channels, NULL}, + {"valid_bit_depth", SND_CONFIG_TYPE_INTEGER, NULL, &valid_bit_depth, NULL}, + {"sample_type", SND_CONFIG_TYPE_INTEGER, NULL, &sample_type, NULL}, + }; + + ret = find_set_values(&ssp_tr_data[0], ARRAY_SIZE(ssp_tr_data), cfg, top, + "Class.Base.tr_stop_config"); + if (ret < 0) + return ret; + + return ssp_tr_stop_set_params(nhlt, sampling_frequency, bit_depth, channel_map, + channel_config, interleaving_style, number_of_channels, + valid_bit_depth,sample_type); +} + +static int set_run_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long always_run; + long ret; + + struct dai_values ssp_run_data[] = { + {"always_run", SND_CONFIG_TYPE_INTEGER, NULL, &always_run, NULL}, + }; + + ret = find_set_values(&ssp_run_data[0], ARRAY_SIZE(ssp_run_data), cfg, top, + "Class.Base.run_config"); + if (ret < 0) + return ret; + + return ssp_run_set_params(nhlt, always_run); +} + +static int set_node_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long sampling_rate; + long node_id; + long ret; + + struct dai_values ssp_node_data[] = { + {"node_id", SND_CONFIG_TYPE_INTEGER, NULL, &node_id, NULL}, + {"sampling_rate", SND_CONFIG_TYPE_INTEGER, NULL, &sampling_rate, NULL}, + }; + + ret = find_set_values(&ssp_node_data[0], ARRAY_SIZE(ssp_node_data), cfg, top, + "Class.Base.node_config"); + if (ret < 0) + return ret; + + return ssp_node_set_params(nhlt, node_id, sampling_rate); +} + +static int set_sync_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long sync_denominator; + long ret; + + struct dai_values ssp_sync_data[] = { + {"sync_denominator", SND_CONFIG_TYPE_INTEGER, NULL, &sync_denominator, NULL}, + }; + + ret = find_set_values(&ssp_sync_data[0], ARRAY_SIZE(ssp_sync_data), cfg, top, + "Class.Base.sync_config"); + if (ret < 0) + return ret; + + return ssp_sync_set_params(nhlt, sync_denominator); +} + +static int set_ext_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long mclk_policy_override; + long mclk_always_running; + long mclk_starts_on_gtw_init; + long mclk_starts_on_run; + long mclk_starts_on_pause; + long mclk_stops_on_pause; + long mclk_stops_on_reset; + + long bclk_policy_override; + long bclk_always_running; + long bclk_starts_on_gtw_init; + long bclk_starts_on_run; + long bclk_starts_on_pause; + long bclk_stops_on_pause; + long bclk_stops_on_reset; + + long sync_policy_override; + long sync_always_running; + long sync_starts_on_gtw_init; + long sync_starts_on_run; + long sync_starts_on_pause; + long sync_stops_on_pause; + long sync_stops_on_reset; + long ret; + + struct dai_values ssp_ext_data[] = { + {"mclk_policy_override", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_policy_override, NULL}, + {"mclk_always_running", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_always_running, NULL}, + {"mclk_starts_on_gtw_init", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_starts_on_gtw_init, NULL}, + {"mclk_starts_on_run", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_starts_on_run, NULL}, + {"mclk_starts_on_pause", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_starts_on_pause, NULL}, + {"mclk_stops_on_pause", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_stops_on_pause, NULL}, + {"mclk_stops_on_reset", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_stops_on_reset, NULL}, + {"bclk_policy_override", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_policy_override, NULL}, + {"bclk_always_running", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_always_running, NULL}, + {"bclk_starts_on_gtw_init", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_starts_on_gtw_init, NULL}, + {"bclk_starts_on_run", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_starts_on_run, NULL}, + {"bclk_starts_on_pause", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_starts_on_pause, NULL}, + {"bclk_stops_on_pause", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_stops_on_pause, NULL}, + {"bclk_stops_on_reset", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_stops_on_reset, NULL}, + {"sync_policy_override", SND_CONFIG_TYPE_INTEGER, NULL, &sync_policy_override, NULL}, + {"sync_always_running", SND_CONFIG_TYPE_INTEGER, NULL, &sync_always_running, NULL}, + {"sync_starts_on_gtw_init", SND_CONFIG_TYPE_INTEGER, NULL, &sync_starts_on_gtw_init, NULL}, + {"sync_starts_on_run", SND_CONFIG_TYPE_INTEGER, NULL, &sync_starts_on_run, NULL}, + {"sync_starts_on_pause", SND_CONFIG_TYPE_INTEGER, NULL, &sync_starts_on_pause, NULL}, + {"sync_stops_on_pause", SND_CONFIG_TYPE_INTEGER, NULL, &sync_stops_on_pause, NULL}, + {"sync_stops_on_reset", SND_CONFIG_TYPE_INTEGER, NULL, &sync_stops_on_reset, NULL}, + }; + + ret = find_set_values(&ssp_ext_data[0], ARRAY_SIZE(ssp_ext_data), cfg, top, + "Class.Base.ext_config"); + if (ret < 0) + return ret; + + return ssp_ext_set_params(nhlt, mclk_policy_override, mclk_always_running, + mclk_starts_on_gtw_init, mclk_starts_on_run, mclk_starts_on_pause, + mclk_stops_on_pause, mclk_stops_on_reset, + bclk_policy_override, bclk_always_running, + bclk_starts_on_gtw_init, bclk_starts_on_run, bclk_starts_on_pause, + bclk_stops_on_pause, bclk_stops_on_reset, + sync_policy_override, sync_always_running, + sync_starts_on_gtw_init, sync_starts_on_run, sync_starts_on_pause, + sync_stops_on_pause, sync_stops_on_reset); +} + +static int set_link_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + long clock_source; + long ret; + + struct dai_values ssp_link_data[] = { + {"clock_source", SND_CONFIG_TYPE_INTEGER, NULL, &clock_source, NULL}, + }; + + ret = find_set_values(&ssp_link_data[0], ARRAY_SIZE(ssp_link_data), cfg, top, + "Class.Base.link_config"); + if (ret < 0) + return ret; + + return ssp_link_set_params(nhlt, clock_source); +} + +static int set_aux_params(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + struct aux_map { + const char *name; + int id; + }; + + struct aux_map aux_maps[] = { + { "Object.Base.mn_config", SSP_MN_DIVIDER_CONTROLS }, + {"Object.Base.clk_config", SSP_DMA_CLK_CONTROLS }, + {"Object.Base.tr_start_config", SSP_DMA_TRANSMISSION_START }, + {"Object.Base.tr_stop_config", SSP_DMA_TRANSMISSION_STOP }, + {"Object.Base.run_config", SSP_DMA_ALWAYS_RUNNING_MODE} , + {"Object.Base.sync_config", SSP_DMA_SYNC_DATA }, + {"Object.Base.ext_config", SSP_DMA_CLK_CONTROLS_EXT }, + {"Object.Base.link_config", SSP_LINK_CLK_SOURCE }, + {"Object.Base.node_config", SSP_DMA_SYNC_NODE }, + }; + + snd_config_iterator_t iter, next; + snd_config_t *items, *n; + const char *id; + unsigned int i; + int ret = 0; + + for (i = 0; i < ARRAY_SIZE(aux_maps); i++) { + if (snd_config_search(cfg, aux_maps[i].name, &items) < 0) + continue; + + snd_config_for_each(iter, next, items) { + n = snd_config_iterator_entry(iter); + + if (snd_config_get_id(n, &id) < 0) + continue; + + switch(aux_maps[i].id) { + case SSP_MN_DIVIDER_CONTROLS: + ret = set_mn_config(nhlt, n, top); + break; + case SSP_DMA_CLK_CONTROLS: + ret = set_clk_config(nhlt, n, top); + break; + case SSP_DMA_TRANSMISSION_START: + ret = set_tr_start_config(nhlt, n, top); + break; + case SSP_DMA_TRANSMISSION_STOP: + ret = set_tr_stop_config(nhlt, n, top); + break; + case SSP_DMA_ALWAYS_RUNNING_MODE: + ret = set_run_config(nhlt, n, top); + break; + case SSP_DMA_SYNC_DATA: + ret = set_sync_config(nhlt, n, top); + break; + case SSP_DMA_CLK_CONTROLS_EXT: + ret = set_ext_config(nhlt, n, top); + break; + case SSP_LINK_CLK_SOURCE: + ret = set_link_config(nhlt, n, top); + break; + case SSP_DMA_SYNC_NODE: + ret = set_node_config(nhlt, n, top); + break; + default: + ret = -EINVAL; + } + + if (ret < 0) + return ret; + } + } + + return ret; +} + +static int set_hw_config(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + const char *format = NULL; + const char *mclk = NULL; + const char *bclk = NULL; + const char *bclk_invert = NULL; + const char *fsync = NULL; + const char *fsync_invert = NULL; + long mclk_freq = 0; + long bclk_freq = 0; + long fsync_freq = 0; + long tdm_slots = 0; + long tdm_slot_width = 0; + long tx_slots = 0; + long rx_slots = 0; + long ret; + + struct dai_values ssp_hw_data[] = { + {"format", SND_CONFIG_TYPE_STRING, NULL, NULL, &format}, + {"mclk", SND_CONFIG_TYPE_STRING, NULL, NULL, &mclk}, + {"bclk", SND_CONFIG_TYPE_STRING, NULL, NULL, &bclk}, + {"fsync", SND_CONFIG_TYPE_STRING, NULL, NULL, &fsync}, + {"bclk_invert", SND_CONFIG_TYPE_STRING, NULL, NULL, &bclk_invert}, + {"fsync_invert", SND_CONFIG_TYPE_STRING, NULL, NULL, &fsync_invert}, + {"fsync_freq", SND_CONFIG_TYPE_INTEGER, NULL, &fsync_freq, NULL}, + {"bclk_freq", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_freq, NULL}, + {"mclk_freq", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_freq, NULL}, + {"tdm_slots", SND_CONFIG_TYPE_INTEGER, NULL, &tdm_slots, NULL}, + {"tdm_slot_width", SND_CONFIG_TYPE_INTEGER, NULL, &tdm_slot_width, NULL}, + {"tx_slots", SND_CONFIG_TYPE_INTEGER, NULL, &tx_slots, NULL}, + {"rx_slots", SND_CONFIG_TYPE_INTEGER, NULL, &rx_slots, NULL}, + }; + + ret = find_set_values(&ssp_hw_data[0], ARRAY_SIZE(ssp_hw_data), cfg, top, + "Class.Base.hw_config"); + if (ret < 0) + return ret; + + ret = set_aux_params(nhlt, cfg, top); + if (ret < 0) + return ret; + + return ssp_hw_set_params(nhlt, format, mclk, bclk, bclk_invert, fsync, fsync_invert, + mclk_freq, bclk_freq, fsync_freq, tdm_slots, tdm_slot_width, + tx_slots, rx_slots); +} + +static int set_ssp_data(struct intel_nhlt_params *nhlt, snd_config_t *dai_cfg, snd_config_t *top) +{ + const char *tdm_padding_per_slot = NULL; + const char *direction = NULL; + const char *quirks = NULL; + long frame_pulse_width = 0; + long clks_control = 0; + long sample_bits = 0; + long bclk_delay = 0; + long version = 0; + long dai_index = 0; + long mclk_id = 0; + long io_clk = 0; + int ret; + + struct dai_values ssp_data[] = { + { "io_clk", SND_CONFIG_TYPE_INTEGER, NULL, &io_clk, NULL}, + { "direction", SND_CONFIG_TYPE_STRING, NULL, NULL, &direction}, + { "quirks", SND_CONFIG_TYPE_STRING, NULL, NULL, &quirks}, + { "dai_index", SND_CONFIG_TYPE_INTEGER, NULL, &dai_index, NULL}, + { "sample_bits", SND_CONFIG_TYPE_INTEGER, NULL, &sample_bits, NULL}, + { "bclk_delay", SND_CONFIG_TYPE_INTEGER, NULL, &bclk_delay, NULL}, + { "mclk_id", SND_CONFIG_TYPE_INTEGER, NULL, &mclk_id, NULL}, + { "clks_control", SND_CONFIG_TYPE_INTEGER, NULL, &clks_control, NULL}, + { "frame_pulse_width", SND_CONFIG_TYPE_INTEGER, NULL, &frame_pulse_width, NULL}, + { "tdm_padding_per_slot", SND_CONFIG_TYPE_STRING, NULL, NULL, + &tdm_padding_per_slot}, + { "version", SND_CONFIG_TYPE_INTEGER, NULL, &version, NULL}, + }; + + ret = find_set_values(&ssp_data[0], ARRAY_SIZE(ssp_data), dai_cfg, top, "Class.Dai.SSP"); + if (ret < 0) + return ret; + + return ssp_set_params(nhlt, direction, dai_index, io_clk, bclk_delay, sample_bits, mclk_id, + clks_control, frame_pulse_width, tdm_padding_per_slot, quirks, + version); +} + +/* init ssp parameters, should be called before parsing dais */ +int nhlt_ssp_init_params(struct intel_nhlt_params *nhlt) +{ + return ssp_init_params(nhlt); +} + +int nhlt_ssp_get_ep_count(struct intel_nhlt_params *nhlt) +{ + return ssp_get_vendor_blob_count(nhlt); +} + +int nhlt_ssp_get_dir(struct intel_nhlt_params *nhlt, int dai_index, uint8_t *dir) +{ + return ssp_get_dir(nhlt, dai_index, dir); +} + +int nhlt_ssp_get_ep(struct intel_nhlt_params *nhlt, struct endpoint_descriptor **eps, + int dai_index, uint8_t dir) +{ + struct endpoint_descriptor ep; + struct ssp_device_specific_config ssp_conf; + struct formats_config f_conf; + struct format_config f_conf1[8]; + uint32_t sample_rate; + uint16_t channel_count; + uint32_t bits_per_sample; + uint32_t virtualbus_id; + uint32_t formats_count; + uint32_t device_type; + uint32_t direction = dir; + uint8_t *ep_target; + size_t blob_size; + int ret; + int i; + + /* + * nhlt ssp structure: + * + * endpoint_descriptor, sizeof(struct endpoint_descriptor) + * device_specific_config (headset), sizeof(struct ssp_device_specific_config) + * formats_config (formats_count), sizeof(struct formats_config) + * format_config (waveex), sizeof(struct format_config) + * vendor_blob sizeof(vendor_blob) + */ + + ret = ssp_get_params(nhlt, dai_index, &virtualbus_id, &formats_count, + &device_type, &direction); + if (ret < 0) { + fprintf(stderr, "nhlt_ssp_get_ep: ssp_get_params failed\n"); + return ret; + } + + ep.link_type = NHLT_LINK_TYPE_SSP; + ep.instance_id = 0; + ep.vendor_id = NHLT_VENDOR_ID_INTEL; + ep.device_id = NHLT_DEVICE_ID_INTEL_I2S_TDM; + ep.revision_id = 0; + ep.subsystem_id = 0; + ep.device_type = device_type; + + ep.direction = direction; + /* ssp device index */ + ep.virtualbus_id = virtualbus_id; + /* ssp config */ + ssp_conf.config.capabilities_size = 2; + ssp_conf.device_config.virtual_slot = 0; + ssp_conf.device_config.config_type = 0; + + /* formats_config */ + f_conf.formats_count = formats_count; + + for (i = 0; i < f_conf.formats_count; i++) { + /* fill in wave format extensible types */ + f_conf1[i].format.wFormatTag = 0xFFFE; + + ret = ssp_get_hw_params(nhlt, dai_index, i, &sample_rate, &channel_count, + &bits_per_sample); + + if (ret < 0) { + fprintf(stderr, "nhlt_ssp_get_ep: ssp_get_hw_params failed\n"); + return ret; + } + + f_conf1[i].format.nChannels = channel_count; + f_conf1[i].format.nSamplesPerSec = sample_rate; + f_conf1[i].format.wBitsPerSample = bits_per_sample; + f_conf1[i].format.nBlockAlign = channel_count * bits_per_sample / 8; + f_conf1[i].format.nAvgBytesPerSec = sample_rate * f_conf1[i].format.nBlockAlign; + + /* bytes after this value in this struct */ + f_conf1[i].format.cbSize = 22; + /* actual bits in container */ + f_conf1[i].format.wValidBitsPerSample = bits_per_sample; + /* channel map not used at this time */ + f_conf1[i].format.dwChannelMask = 0; + /* WAVE_FORMAT_PCM guid (0x0001) ? */ + f_conf1[i].format.SubFormat[0] = 0; + f_conf1[i].format.SubFormat[1] = 0; + f_conf1[i].format.SubFormat[2] = 0; + f_conf1[i].format.SubFormat[3] = 0; + + ret = ssp_get_vendor_blob_size(nhlt, dai_index, i, &blob_size); + if (ret < 0) { + fprintf(stderr, "nhlt_ssp_get_ep: dmic_get_vendor_blob_size failed\n"); + return ret; + } + f_conf1[i].vendor_blob.capabilities_size = blob_size; + } + + ep.length = sizeof(struct endpoint_descriptor) + + sizeof(struct ssp_device_specific_config) + + sizeof(struct formats_config) + + sizeof(struct format_config) * f_conf.formats_count + + blob_size * f_conf.formats_count; + + /* allocate the final variable length ep struct */ + ep_target = calloc(ep.length, sizeof(uint8_t)); + if (!ep_target) + return -ENOMEM; + + *eps = (struct endpoint_descriptor *)ep_target; + + /* copy all parsed sub arrays into the top level array */ + memcpy(ep_target, &ep, sizeof(struct endpoint_descriptor)); + + ep_target += sizeof(struct endpoint_descriptor); + + memcpy(ep_target, &ssp_conf, sizeof(struct ssp_device_specific_config)); + ep_target += sizeof(struct ssp_device_specific_config); + + memcpy(ep_target, &f_conf, sizeof(struct formats_config)); + ep_target += sizeof(struct formats_config); + + /* copy all hw configs */ + for (i = 0; i < f_conf.formats_count; i++) { + memcpy(ep_target, &f_conf1[i], sizeof(struct format_config)); + ep_target += sizeof(struct format_config); + ret = ssp_get_vendor_blob(nhlt, ep_target, dai_index, i); + if (ret < 0) { + fprintf(stderr, "nhlt_sso_get_ep: ssp_get_vendor_blob failed\n"); + return ret; + } + ep_target += blob_size; + } + + return 0; +} + +/* Set ssp parameters from topology for ssp coefficient calculation. + * + * You can see an example of topology v2 config of ssp below. In this example the default + * object parameters are spelled out for clarity. General parameters like sample_bits are parsed + * with set_ssp_data and hw_config object data with set_hw_data. Ssp can have multiple hw_configs. + * Values are saved into intermediate structs and the vendor specific blob is calculated at the end + * of parsing with ssp_calculate. + * + * SSP."0" { + * id 0 + * direction "duplex" + * name NoCodec-0 + * io_clk 38400000 + * default_hw_conf_id 0 + * sample_bits 16 + * quirks "lbm_mode" + * bclk_delay 0 + * mclk_id 0 + * clks_control 0 + * frame_pulse_width 0 + * tdm_padding_per_slot false + * + * Object.Base.hw_config."SSP0" { + * id 0 + * mclk_freq 24576000 + * bclk_freq 3072000 + * tdm_slot_width 32 + * format "I2S" + * mclk "codec_mclk_in" + * bclk "codec_consumer" + * fsync "codec_consumer" + * fsync_freq 48000 + * tdm_slots 2 + * tx_slots 3 + * rx_slots 3 + * } + * } + */ +int nhlt_ssp_set_params(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top) +{ + snd_config_iterator_t i, next; + snd_config_t *items; + snd_config_t *n; + const char *id; + int ret; + + ret = set_ssp_data(nhlt, cfg, top); + if (ret < 0) + return ret; + + ret = snd_config_search(cfg, "Object.Base.hw_config", &items); + if (ret < 0) + return ret; + + snd_config_for_each(i, next, items) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + ret = set_hw_config(nhlt, n, top); + if (ret < 0) + return ret; + } + + ret = ssp_calculate(nhlt); + + return ret; +} diff --git a/topology/nhlt/intel/ssp-nhlt.h b/topology/nhlt/intel/ssp-nhlt.h new file mode 100644 index 0000000..456bfca --- /dev/null +++ b/topology/nhlt/intel/ssp-nhlt.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#ifndef __SSP_NHLT_H +#define __SSP_NHLT_H + +#include "intel-nhlt.h" +#include "../nhlt.h" + +int nhlt_ssp_init_params(struct intel_nhlt_params *nhlt); +int nhlt_ssp_set_params(struct intel_nhlt_params *nhlt, snd_config_t *cfg, snd_config_t *top); +int nhlt_ssp_get_ep(struct intel_nhlt_params *nhlt, struct endpoint_descriptor **eps, + int dai_index, uint8_t dir); +int nhlt_ssp_get_ep_count(struct intel_nhlt_params *nhlt); +int nhlt_ssp_get_dir(struct intel_nhlt_params *nhlt, int dai_index, uint8_t *dir); +#endif diff --git a/topology/nhlt/intel/ssp/ssp-debug.c b/topology/nhlt/intel/ssp/ssp-debug.c new file mode 100644 index 0000000..afb8ac2 --- /dev/null +++ b/topology/nhlt/intel/ssp/ssp-debug.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include "ssp-debug.h" +#include "../intel-nhlt.h" + +#ifdef NHLT_DEBUG + +static void ssp_print_blob10(struct intel_ssp_params *ssp, int i) +{ + struct ssp_intel_config_data *blob; + int ssp_index = ssp->ssp_count; + int j; + + blob = &ssp->ssp_blob[ssp_index][i]; + fprintf(stdout, "gateway_attributes %u\n", blob->gateway_attributes); + for (j = 0; j < SSP_TS_GROUP_SIZE; j++) + fprintf(stdout, "ts_group[%d] 0x%08x\n", j, blob->ts_group[j]); + + fprintf(stdout, "ssc0 0x%08x\n", blob->ssc0); + fprintf(stdout, "ssc1 0x%08x\n", blob->ssc1); + fprintf(stdout, "sscto 0x%08x\n", blob->sscto); + fprintf(stdout, "sspsp 0x%08x\n", blob->sspsp); + fprintf(stdout, "sstsa 0x%08x\n", blob->sstsa); + fprintf(stdout, "ssrsa 0x%08x\n", blob->ssrsa); + fprintf(stdout, "ssc2 0x%08x\n", blob->ssc2); + fprintf(stdout, "sspsp2 0x%08x\n", blob->sspsp2); + fprintf(stdout, "ssc3 0x%08x\n", blob->ssc3); + fprintf(stdout, "ssioc 0x%08x\n", blob->ssioc); + fprintf(stdout, "mdivc 0x%08x\n", blob->mdivc); + fprintf(stdout, "mdivr 0x%08x\n", blob->mdivr); +} + +static void ssp_print_blob15(struct intel_ssp_params *ssp, int i) +{ + struct ssp_intel_config_data_1_5 *blob15; + int ssp_index = ssp->ssp_count; + uint32_t j; + + blob15 = &ssp->ssp_blob_1_5[ssp_index][i]; + fprintf(stdout, "gateway_attributes %u\n", blob15->gateway_attributes); + fprintf(stdout, "version %u\n", blob15->version); + fprintf(stdout, "size %u\n", blob15->size); + for (j = 0; j < SSP_TS_GROUP_SIZE; j++) + fprintf(stdout, "ts_group[%d] 0x%08x\n", j, blob15->ts_group[j]); + + fprintf(stdout, "ssc0 0x%08x\n", blob15->ssc0); + fprintf(stdout, "ssc1 0x%08x\n", blob15->ssc1); + fprintf(stdout, "sscto 0x%08x\n", blob15->sscto); + fprintf(stdout, "sspsp 0x%08x\n", blob15->sspsp); + fprintf(stdout, "sstsa 0x%08x\n", blob15->sstsa); + fprintf(stdout, "ssrsa 0x%08x\n", blob15->ssrsa); + fprintf(stdout, "ssc2 0x%08x\n", blob15->ssc2); + fprintf(stdout, "sspsp2 0x%08x\n", blob15->sspsp2); + fprintf(stdout, "ssc3 0x%08x\n", blob15->ssc3); + fprintf(stdout, "ssioc 0x%08x\n", blob15->ssioc); + fprintf(stdout, "mdivc 0x%08x\n", blob15->mdivctlr); + fprintf(stdout, "mdivr count 0x%08x\n", blob15->mdivrcnt); + for (j = 0; j < blob15->mdivrcnt; j++) + fprintf(stdout, "mdivr 0x%08x\n", + ssp->ssp_prm[ssp_index].mdivr[i].mdivrs[j]); +} + +static void ssp_print_blob30(struct intel_ssp_params *ssp, int i) +{ + struct ssp_intel_config_data_3_0 *blob30; + int ssp_index = ssp->ssp_count; + uint32_t j; + + blob30 = &ssp->ssp_blob_3_0[ssp_index][i]; + fprintf(stdout, "gateway_attributes %u\n", blob30->gateway_attributes); + fprintf(stdout, "version %u\n", blob30->version); + fprintf(stdout, "size %u\n", blob30->size); + for (j = 0; j < SSP_TS_GROUP_SIZE; j++) + fprintf(stdout, "ts_group[%d] 0x%08x\n", j, blob30->ts_group[j]); + + fprintf(stdout, "ssc0 0x%08x\n", blob30->ssc0); + fprintf(stdout, "ssc1 0x%08x\n", blob30->ssc1); + fprintf(stdout, "sscto 0x%08x\n", blob30->sscto); + fprintf(stdout, "sspsp 0x%08x\n", blob30->sspsp); + fprintf(stdout, "ssc2 0x%08x\n", blob30->ssc2); + fprintf(stdout, "sspsp2 0x%08x\n", blob30->sspsp2); + fprintf(stdout, "rsvd2 0x%08x\n", blob30->rsvd2); + fprintf(stdout, "ssioc 0x%08x\n", blob30->ssioc); + for (j = 0; j < I2SIPCMC; j++) { + fprintf(stdout, "rx_dir[%d] 0x%08x\n", j * 2, (uint32_t)(blob30->rx_dir[j].ssmidytsa & 0xffffffff)); + fprintf(stdout, "rx_dir[%d] 0x%08x\n", j * 2 + 1, (uint32_t)(blob30->rx_dir[j].ssmidytsa >> 32)); + } + for (j = 0; j < I2SOPCMC; j++) { + fprintf(stdout, "tx_dir[%d] 0x%08x\n", j * 2, (uint32_t)(blob30->tx_dir[j].ssmodytsa & 0xffffffff)); + fprintf(stdout, "tx_dir[%d] 0x%08x\n", j * 2 + 1, (uint32_t)(blob30->tx_dir[j].ssmodytsa >> 32)); + } + + fprintf(stdout, "mdivc 0x%08x\n", blob30->mdivctlr); + fprintf(stdout, "mdivr count 0x%08x\n", blob30->mdivrcnt); + for (j = 0; j < blob30->mdivrcnt; j++) + fprintf(stdout, "mdivr 0x%08x\n", + ssp->ssp_prm[ssp_index].mdivr[i].mdivrs[j]); +} + +void ssp_print_calculated(struct intel_ssp_params *ssp) +{ + struct ssp_aux_blob *blob_aux; + int ssp_index = ssp->ssp_count; + uint32_t *ptr; + uint32_t i, j; + + fprintf(stdout, "printing ssp nhlt calculated data:\n"); + + /* top level struct */ + fprintf(stdout, "ssp index %d\n", ssp_index); + + fprintf(stdout, "ssp %d dai_index: %u\n", ssp_index, ssp->ssp_dai_index[ssp_index]); + + fprintf(stdout, "ssp blob version %u\n", ssp->ssp_prm[ssp_index].version); + + fprintf(stdout, "ssp %d hw_config_count: %u\n", ssp_index, + ssp->ssp_hw_config_count[ssp_index]); + + fprintf(stdout, "\n"); + + for (i = 0; i < ssp->ssp_hw_config_count[ssp_index]; i++) { + fprintf(stdout, "ssp blob %d hw_config %d\n", ssp->ssp_count, i); + switch (ssp->ssp_prm[ssp_index].version) { + case SSP_BLOB_VER_1_5: + ssp_print_blob15(ssp, i); + break; + case SSP_BLOB_VER_3_0: + ssp_print_blob30(ssp, i); + break; + default: + ssp_print_blob10(ssp, i); + } + + blob_aux = (struct ssp_aux_blob *)&(ssp->ssp_blob_ext[ssp_index][i]); + fprintf(stdout, "aux_blob size %u\n", blob_aux->size); + for (j = 0; j < blob_aux->size; j += 4) { + ptr = (uint32_t *)&(blob_aux->aux_blob[j]); + fprintf(stdout, "aux_blob %d 0x%08x\n", j, *ptr); + } + + fprintf(stdout, "\n"); + } + + fprintf(stdout, "\n"); +} + +void ssp_print_internal(struct intel_ssp_params *ssp) +{ + struct ssp_aux_config_link *link; + struct ssp_aux_config_sync *sync; + struct ssp_aux_config_ext *ext; + struct ssp_aux_config_run *run; + struct ssp_aux_config_clk *clk; + struct ssp_aux_config_mn *mn; + struct ssp_aux_config_tr *tr; + struct ssp_config_dai *dai; + struct ssp_config_hw *hw_conf; + uint32_t enabled; + uint32_t i, j; + + dai = &ssp->ssp_prm[ssp->ssp_count]; + + fprintf(stdout, "printing ssp nhlt internal data:\n"); + + fprintf(stdout, "io_clk %u\n", dai->io_clk); + fprintf(stdout, "dai_index %u\n", dai->dai_index); + fprintf(stdout, "mclk_id %u\n", dai->mclk_id); + fprintf(stdout, "sample_valid_bits %u\n", dai->sample_valid_bits); + fprintf(stdout, "mclk_direction %u\n", dai->mclk_direction); + fprintf(stdout, "frame_pulse_width %u\n", dai->frame_pulse_width); + fprintf(stdout, "tdm_per_slot_padding_flag %u\n", dai->tdm_per_slot_padding_flag); + fprintf(stdout, "clks_control %u\n", dai->clks_control); + fprintf(stdout, "quirks %u\n", dai->quirks); + fprintf(stdout, "bclk_delay %u\n", dai->bclk_delay); + fprintf(stdout, "version %u\n", dai->version); + + fprintf(stdout, "\n"); + + fprintf(stdout, "hw_config_count %u\n", ssp->ssp_hw_config_count[ssp->ssp_count]); + + for (i = 0; i < ssp->ssp_hw_config_count[ssp->ssp_count]; i++) { + hw_conf = &dai->hw_cfg[i]; + fprintf(stdout, "mclk_rate %u\n", hw_conf->mclk_rate); + fprintf(stdout, "bclk_rate %u\n", hw_conf->bclk_rate); + fprintf(stdout, "fsync_rate %u\n", hw_conf->fsync_rate); + fprintf(stdout, "tdm_slots %u\n", hw_conf->tdm_slots); + fprintf(stdout, "tdm_slot_width %u\n", hw_conf->tdm_slot_width); + fprintf(stdout, "tx_slots %u\n", hw_conf->tx_slots); + fprintf(stdout, "rx_slots %u\n", hw_conf->rx_slots); + fprintf(stdout, "format %u\n", hw_conf->format); + + enabled = dai->aux_cfg[i].enabled; + fprintf(stdout, "aux enabled %x\n", enabled); + fprintf(stdout, "\n"); + + mn = (struct ssp_aux_config_mn *)&(dai->aux_cfg[i].mn); + clk = (struct ssp_aux_config_clk *)&(dai->aux_cfg[i].clk); + tr = (struct ssp_aux_config_tr *)&(dai->aux_cfg[i].tr_start); + tr = (struct ssp_aux_config_tr *)&(dai->aux_cfg[i].tr_stop); + run = (struct ssp_aux_config_run *)&(dai->aux_cfg[i].run); + sync = (struct ssp_aux_config_sync *)&(dai->aux_cfg[i].sync); + ext = (struct ssp_aux_config_ext *)&(dai->aux_cfg[i].ext); + link = (struct ssp_aux_config_link *)&(dai->aux_cfg[i].link); + + if (enabled & BIT(SSP_MN_DIVIDER_CONTROLS)) { + fprintf(stdout, "aux mn m_div %u\n", mn->m_div); + fprintf(stdout, "aux mn n_div %u\n", mn->n_div); + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_DMA_CLK_CONTROLS)) { + fprintf(stdout, "aux clk clock_warm_up %u\n", clk->clock_warm_up); + fprintf(stdout, "aux clk mclk %u\n", clk->mclk); + fprintf(stdout, "aux clk warm_up_ovr %u\n", clk->warm_up_ovr); + fprintf(stdout, "aux clk clock_stop_delay %u\n", clk->clock_stop_delay); + fprintf(stdout, "aux clk keep_running %u\n", clk->keep_running); + fprintf(stdout, "aux clk keep_running %u\n", clk->clock_stop_ovr); + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_DMA_TRANSMISSION_START)) { + fprintf(stdout, "aux tr start sampling_frequency %u\n", tr->sampling_frequency); + fprintf(stdout, "aux tr start bit_depth %u\n", tr->bit_depth); + fprintf(stdout, "aux tr start channel_map %u\n", tr->channel_map); + fprintf(stdout, "aux tr start channel_config %u\n", tr->channel_config); + fprintf(stdout, "aux tr start interleaving_style %u\n", tr->interleaving_style); + fprintf(stdout, "aux tr start number_of_channels %u\n", tr->number_of_channels); + fprintf(stdout, "aux tr start valid_bit_depth %u\n", tr->valid_bit_depth); + fprintf(stdout, "aux tr start sample_types %u\n", tr->sample_type); + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_DMA_TRANSMISSION_STOP)) { + fprintf(stdout, "aux tr start sampling_frequency %u\n", tr->sampling_frequency); + fprintf(stdout, "aux tr start bit_depth %u\n", tr->bit_depth); + fprintf(stdout, "aux tr start channel_map %u\n", tr->channel_map); + fprintf(stdout, "aux tr start channel_config %u\n", tr->channel_config); + fprintf(stdout, "aux tr start interleaving_style %u\n", tr->interleaving_style); + fprintf(stdout, "aux tr start number_of_channels %u\n", tr->number_of_channels); + fprintf(stdout, "aux tr start valid_bit_depth %u\n", tr->valid_bit_depth); + fprintf(stdout, "aux tr start sample_types %u\n", tr->sample_type); + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_DMA_ALWAYS_RUNNING_MODE)) { + fprintf(stdout, "aux run always_run %u\n", run->always_run); + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_DMA_SYNC_DATA)) { + fprintf(stdout, "aux sync sync_denominator %u\n", sync->sync_denominator); + fprintf(stdout, "aux sync count %u\n", sync->count); + + for (j = 0; j < sync->count; j++) { + fprintf(stdout, "aux sync node_id %u\n", sync->nodes[j].node_id); + fprintf(stdout, "aux sync sampling_rate %u\n", sync->nodes[j].sampling_rate); + } + + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_DMA_CLK_CONTROLS_EXT)) { + fprintf(stdout, "aux ext mclk_policy_override %u\n", ext->mclk_policy_override); + fprintf(stdout, "aux ext mclk_always_running %u\n", ext->mclk_always_running); + fprintf(stdout, "aux ext mclk_starts_on_gtw_init %u\n", ext->mclk_starts_on_gtw_init); + fprintf(stdout, "aux ext mclk_starts_on_run %u\n", ext->mclk_starts_on_run); + fprintf(stdout, "aux ext mclk_starts_on_pause %u\n", ext->mclk_starts_on_pause); + fprintf(stdout, "aux ext mclk_stops_on_pause %u\n", ext->mclk_stops_on_pause); + fprintf(stdout, "aux ext mclk_stops_on_reset %u\n", ext->mclk_stops_on_reset); + fprintf(stdout, "aux ext bclk_policy_override %u\n", ext->bclk_policy_override); + fprintf(stdout, "aux ext bclk_always_running %u\n", ext->bclk_always_running); + fprintf(stdout, "aux ext bclk_starts_on_gtw_init %u\n", ext->bclk_starts_on_gtw_init); + fprintf(stdout, "aux ext bclk_starts_on_run %u\n", ext->bclk_starts_on_run); + fprintf(stdout, "aux ext bclk_starts_on_pause %u\n", ext->bclk_starts_on_pause); + fprintf(stdout, "aux ext bclk_stops_on_pause %u\n", ext->bclk_stops_on_pause); + fprintf(stdout, "aux ext bclk_stops_on_reset %u\n", ext->bclk_stops_on_reset); + fprintf(stdout, "aux ext sync_policy_override %u\n", ext->sync_policy_override); + fprintf(stdout, "aux ext sync_always_running %u\n", ext->sync_always_running); + fprintf(stdout, "aux ext sync_starts_on_gtw_init %u\n", ext->sync_starts_on_gtw_init); + fprintf(stdout, "aux ext sync_starts_on_run %u\n", ext->sync_starts_on_run); + fprintf(stdout, "aux ext sync_starts_on_pause %u\n", ext->sync_starts_on_pause); + fprintf(stdout, "aux ext sync_stops_on_pause %u\n", ext->sync_stops_on_pause); + fprintf(stdout, "aux ext sync_stops_on_reset %u\n", ext->sync_stops_on_reset); + fprintf(stdout, "\n"); + } + + if (enabled & BIT(SSP_LINK_CLK_SOURCE)) { + fprintf(stdout, "aux link clock_source %u\n", link->clock_source); + fprintf(stdout, "\n"); + } + } + + fprintf(stdout, "\n"); +} + +#else /* NHLT_DEBUG */ +void ssp_print_internal(struct intel_ssp_params *ssp ATTRIBUTE_UNUSED) {} +void ssp_print_calculated(struct intel_ssp_params *ssp ATTRIBUTE_UNUSED) {} +#endif diff --git a/topology/nhlt/intel/ssp/ssp-debug.h b/topology/nhlt/intel/ssp/ssp-debug.h new file mode 100644 index 0000000..d1a8df6 --- /dev/null +++ b/topology/nhlt/intel/ssp/ssp-debug.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#ifndef __SSP_DEBUG_H +#define __SSP_DEBUG_H + +#include "ssp-internal.h" + +void ssp_print_internal(struct intel_ssp_params *ssp); +void ssp_print_calculated(struct intel_ssp_params *ssp); + +#endif diff --git a/topology/nhlt/intel/ssp/ssp-intel.h b/topology/nhlt/intel/ssp/ssp-intel.h new file mode 100644 index 0000000..ac141c8 --- /dev/null +++ b/topology/nhlt/intel/ssp/ssp-intel.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Liam Girdwood +// Keyon Jie +// Rander Wang +// Jaska Uimonen + +#ifndef __SSP_INTEL_H +#define __SSP_INTEL_H + +#include + +#define SSP_TS_GROUP_SIZE 8 + +/* struct for intel ssp nhlt vendor specific blob generation */ +struct ssp_intel_config_data { + uint32_t gateway_attributes; + uint32_t ts_group[SSP_TS_GROUP_SIZE]; + uint32_t ssc0; + uint32_t ssc1; + uint32_t sscto; + uint32_t sspsp; + uint32_t sstsa; + uint32_t ssrsa; + uint32_t ssc2; + uint32_t sspsp2; + uint32_t ssc3; + uint32_t ssioc; + uint32_t mdivc; + uint32_t mdivr; +} __attribute__((packed)); + +#define SSP_BLOB_VER_1_5 0xEE000105 +#define SSP_BLOB_VER_3_0 0xEE000300 + +struct ssp_intel_config_data_1_5 { + uint32_t gateway_attributes; + uint32_t version; + uint32_t size; + uint32_t ts_group[SSP_TS_GROUP_SIZE]; + uint32_t ssc0; + uint32_t ssc1; + uint32_t sscto; + uint32_t sspsp; + uint32_t sstsa; + uint32_t ssrsa; + uint32_t ssc2; + uint32_t sspsp2; + uint32_t ssc3; + uint32_t ssioc; + uint32_t mdivctlr; + uint32_t mdivrcnt; + uint32_t mdivr[]; +} __attribute__((packed)); + +#define I2SIPCMC 8 +#define I2SOPCMC 8 + +struct ssp_rx_dir { + uint64_t ssmidytsa; +} __attribute__((packed)); + +struct ssp_tx_dir { + uint64_t ssmodytsa; +} __attribute__((packed)); + +struct ssp_intel_config_data_3_0 { + uint32_t gateway_attributes; + uint32_t version; + uint32_t size; + uint32_t ts_group[SSP_TS_GROUP_SIZE]; + uint32_t ssc0; + uint32_t ssc1; + uint32_t sscto; + uint32_t sspsp; + uint32_t ssc2; + uint32_t sspsp2; + uint32_t rsvd2; + uint32_t ssioc; + struct ssp_rx_dir rx_dir[I2SIPCMC]; + struct ssp_tx_dir tx_dir[I2SOPCMC]; + uint32_t mdivctlr; + uint32_t mdivrcnt; + uint32_t mdivr[]; +} __attribute__((packed)); + +struct ssp_intel_aux_tlv { + uint32_t type; + uint32_t size; + uint32_t val[]; +} __attribute__((packed)); + +struct ssp_intel_mn_ctl { + uint32_t div_m; + uint32_t div_n; +} __attribute__((packed)); + +struct ssp_intel_clk_ctl { + uint32_t start; + uint32_t stop; +} __attribute__((packed)); + +struct ssp_intel_tr_ctl { + uint32_t sampling_frequency; + uint32_t bit_depth; + uint32_t channel_map; + uint32_t channel_config; + uint32_t interleaving_style; + uint32_t format; +} __attribute__((packed)); + +struct ssp_intel_run_ctl { + uint32_t enabled; +} __attribute__((packed)); + +struct ssp_intel_node_ctl { + uint32_t node_id; + uint32_t sampling_rate; +} __attribute__((packed)); + +struct ssp_intel_sync_ctl { + uint32_t sync_denominator; + uint32_t count; +} __attribute__((packed)); + +struct ssp_intel_ext_ctl { + uint32_t ext_data; +} __attribute__((packed)); + +struct ssp_intel_link_ctl { + uint32_t clock_source; +} __attribute__((packed)); + +#endif /* __SSP_INTEL_H */ diff --git a/topology/nhlt/intel/ssp/ssp-internal.h b/topology/nhlt/intel/ssp/ssp-internal.h new file mode 100644 index 0000000..82815bc --- /dev/null +++ b/topology/nhlt/intel/ssp/ssp-internal.h @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Liam Girdwood +// Keyon Jie +// Rander Wang +// Jaska Uimonen + +#ifndef __SSP_MACROS_H +#define __SSP_MACROS_H + +#include "ssp-intel.h" + +#define SSP_MAX_DAIS 8 +#define SSP_MAX_HW_CONFIG 8 +#define SSP_TDM_MAX_SLOT_MAP_COUNT 8 + +struct ssp_aux_config_mn { + uint32_t m_div; + uint32_t n_div; +}; + +struct ssp_aux_config_clk { + uint32_t clock_warm_up; + uint32_t mclk; + uint32_t warm_up_ovr; + uint32_t clock_stop_delay; + uint32_t keep_running; + uint32_t clock_stop_ovr; +}; + +struct ssp_aux_config_tr { + uint32_t sampling_frequency; + uint32_t bit_depth; + uint32_t channel_map; + uint32_t channel_config; + uint32_t interleaving_style; + uint32_t number_of_channels; + uint32_t valid_bit_depth; + uint32_t sample_type; +}; + +struct ssp_aux_config_run { + uint32_t always_run; +}; + +struct ssp_aux_config_node { + uint32_t node_id; + uint32_t sampling_rate; +}; + +struct ssp_aux_config_sync { + uint32_t sync_denominator; + uint32_t count; + struct ssp_aux_config_node nodes[SSP_MAX_DAIS]; +}; + +struct ssp_aux_config_ext { + uint32_t mclk_policy_override; + uint32_t mclk_always_running; + uint32_t mclk_starts_on_gtw_init; + uint32_t mclk_starts_on_run; + uint32_t mclk_starts_on_pause; + uint32_t mclk_stops_on_pause; + uint32_t mclk_stops_on_reset; + uint32_t bclk_policy_override; + uint32_t bclk_always_running; + uint32_t bclk_starts_on_gtw_init; + uint32_t bclk_starts_on_run; + uint32_t bclk_starts_on_pause; + uint32_t bclk_stops_on_pause; + uint32_t bclk_stops_on_reset; + uint32_t sync_policy_override; + uint32_t sync_always_running; + uint32_t sync_starts_on_gtw_init; + uint32_t sync_starts_on_run; + uint32_t sync_starts_on_pause; + uint32_t sync_stops_on_pause; + uint32_t sync_stops_on_reset; +}; + +struct ssp_aux_config_link { + uint32_t clock_source; +}; + +struct ssp_config_aux { + /* bits set for found aux structs */ + uint32_t enabled; + struct ssp_aux_config_mn mn; + struct ssp_aux_config_clk clk; + struct ssp_aux_config_tr tr_start; + struct ssp_aux_config_tr tr_stop; + struct ssp_aux_config_run run; + struct ssp_aux_config_sync sync; + struct ssp_aux_config_ext ext; + struct ssp_aux_config_link link; +}; + +struct ssp_aux_blob { + uint32_t size; + uint8_t aux_blob[256]; +}; + +struct ssp_config_mdivr { + uint32_t count; + uint32_t mdivrs[8]; +}; + +/* structs for gathering the ssp parameters from topology */ +struct ssp_config_hw { + uint32_t mclk_rate; + uint32_t bclk_rate; + uint32_t fsync_rate; + uint32_t tdm_slots; + uint32_t tdm_slot_width; + uint32_t tx_slots; + uint32_t rx_slots; + uint32_t format; +}; + +struct ssp_config_dai { + uint32_t io_clk; + uint32_t dai_index; + uint16_t mclk_id; + uint32_t sample_valid_bits; + uint32_t mclk_direction; + uint16_t frame_pulse_width; + uint16_t tdm_per_slot_padding_flag; + uint32_t clks_control; + uint32_t quirks; + uint32_t bclk_delay; + uint8_t direction; + uint32_t version; + struct ssp_config_hw hw_cfg[SSP_MAX_HW_CONFIG]; + struct ssp_config_aux aux_cfg[SSP_MAX_HW_CONFIG]; + struct ssp_config_mdivr mdivr[SSP_MAX_HW_CONFIG]; +}; + +struct intel_ssp_params { + /* structs to gather ssp params before calculations */ + struct ssp_config_dai ssp_prm[SSP_MAX_DAIS]; + uint32_t ssp_dai_index[SSP_MAX_DAIS]; + uint32_t ssp_hw_config_count[SSP_MAX_DAIS]; + int ssp_count; + + /* ssp vendor blob structs */ + struct ssp_intel_config_data ssp_blob[SSP_MAX_DAIS][SSP_MAX_HW_CONFIG]; + struct ssp_intel_config_data_1_5 ssp_blob_1_5[SSP_MAX_DAIS][SSP_MAX_HW_CONFIG]; + struct ssp_intel_config_data_3_0 ssp_blob_3_0[SSP_MAX_DAIS][SSP_MAX_HW_CONFIG]; + struct ssp_aux_blob ssp_blob_ext[SSP_MAX_DAIS][SSP_MAX_HW_CONFIG]; +}; + +#define SSP_MN_DIVIDER_CONTROLS 0 +#define SSP_DMA_CLK_CONTROLS 1 +#define SSP_DMA_TRANSMISSION_START 2 +#define SSP_DMA_TRANSMISSION_STOP 3 +#define SSP_DMA_ALWAYS_RUNNING_MODE 4 +#define SSP_DMA_SYNC_DATA 5 +#define SSP_DMA_CLK_CONTROLS_EXT 6 +#define SSP_LINK_CLK_SOURCE 7 +/* officially "undefined" node for topology parsing */ +#define SSP_DMA_SYNC_NODE 32 + +#define SSP_CLOCK_XTAL_OSCILLATOR 0x0 +#define SSP_CLOCK_AUDIO_CARDINAL 0x1 +#define SSP_CLOCK_PLL_FIXED 0x2 + +#define MCDSS(x) SET_BITS(17, 16, x) +#define MNDSS(x) SET_BITS(21, 20, x) + +#define SSP_FMT_I2S 1 /**< I2S mode */ +#define SSP_FMT_RIGHT_J 2 /**< Right Justified mode */ +#define SSP_FMT_LEFT_J 3 /**< Left Justified mode */ +#define SSP_FMT_DSP_A 4 /**< L data MSB after FRM LRC */ +#define SSP_FMT_DSP_B 5 /**< L data MSB during FRM LRC */ +#define SSP_FMT_PDM 6 /**< Pulse density modulation */ + +#define SSP_FMT_CONT (1 << 4) /**< continuous clock */ +#define SSP_FMT_GATED (0 << 4) /**< clock is gated */ + +#define SSP_FMT_NB_NF (0 << 8) /**< normal bit clock + frame */ +#define SSP_FMT_NB_IF (2 << 8) /**< normal BCLK + inv FRM */ +#define SSP_FMT_IB_NF (3 << 8) /**< invert BCLK + nor FRM */ +#define SSP_FMT_IB_IF (4 << 8) /**< invert BCLK + FRM */ + +#define SSP_FMT_CBP_CFP (0 << 12) /**< codec bclk provider & frame provider */ +#define SSP_FMT_CBC_CFP (2 << 12) /**< codec bclk consumer & frame provider */ +#define SSP_FMT_CBP_CFC (3 << 12) /**< codec bclk provider & frame consumer */ +#define SSP_FMT_CBC_CFC (4 << 12) /**< codec bclk consumer & frame consumer */ + +#define SSP_FMT_FORMAT_MASK 0x000f +#define SSP_FMT_CLOCK_MASK 0x00f0 +#define SSP_FMT_INV_MASK 0x0f00 +#define SSP_FMT_CLOCK_PROVIDER_MASK 0xf000 + +/* SSCR0 bits */ +#define SSCR0_DSIZE(x) SET_BITS(3, 0, (x) - 1) +#define SSCR0_FRF MASK(5, 4) +#define SSCR0_MOT SET_BITS(5, 4, 0) +#define SSCR0_TI SET_BITS(5, 4, 1) +#define SSCR0_NAT SET_BITS(5, 4, 2) +#define SSCR0_PSP SET_BITS(5, 4, 3) +#define SSCR0_ECS BIT(6) +#define SSCR0_SSE BIT(7) +#define SSCR0_SCR_MASK MASK(19, 8) +#define SSCR0_SCR(x) SET_BITS(19, 8, x) +#define SSCR0_EDSS BIT(20) +#define SSCR0_NCS BIT(21) +#define SSCR0_RIM BIT(22) +#define SSCR0_TIM BIT(23) +#define SSCR0_FRDC(x) SET_BITS(26, 24, (x) - 1) +#define SSCR0_ACS BIT(30) +#define SSCR0_MOD BIT(31) + +/* SSCR1 bits */ +#define SSCR1_RIE BIT(0) +#define SSCR1_TIE BIT(1) +#define SSCR1_LBM BIT(2) +#define SSCR1_SPO BIT(3) +#define SSCR1_SPH BIT(4) +#define SSCR1_MWDS BIT(5) +#define SSCR1_TFT_MASK MASK(9, 6) +#define SSCR1_TFT(x) SET_BITS(9, 6, (x) - 1) +#define SSCR1_RFT_MASK MASK(13, 10) +#define SSCR1_RFT(x) SET_BITS(13, 10, (x) - 1) +#define SSCR1_EFWR BIT(14) +#define SSCR1_STRF BIT(15) +#define SSCR1_IFS BIT(16) +#define SSCR1_PINTE BIT(18) +#define SSCR1_TINTE BIT(19) +#define SSCR1_RSRE BIT(20) +#define SSCR1_TSRE BIT(21) +#define SSCR1_TRAIL BIT(22) +#define SSCR1_RWOT BIT(23) +#define SSCR1_SFRMDIR BIT(24) +#define SSCR1_SCLKDIR BIT(25) +#define SSCR1_ECRB BIT(26) +#define SSCR1_ECRA BIT(27) +#define SSCR1_SCFR BIT(28) +#define SSCR1_EBCEI BIT(29) +#define SSCR1_TTE BIT(30) +#define SSCR1_TTELP BIT(31) + +/* SSCR2 bits */ +#define SSCR2_URUN_FIX0 BIT(0) +#define SSCR2_URUN_FIX1 BIT(1) +#define SSCR2_SLV_EXT_CLK_RUN_EN BIT(2) +#define SSCR2_CLK_DEL_EN BIT(3) +#define SSCR2_UNDRN_FIX_EN BIT(6) +#define SSCR2_FIFO_EMPTY_FIX_EN BIT(7) +#define SSCR2_ASRC_CNTR_EN BIT(8) +#define SSCR2_ASRC_CNTR_CLR BIT(9) +#define SSCR2_ASRC_FRM_CNRT_EN BIT(10) +#define SSCR2_ASRC_INTR_MASK BIT(11) +#define SSCR2_TURM1 BIT(1) +#define SSCR2_PSPSRWFDFD BIT(3) +#define SSCR2_PSPSTWFDFD BIT(4) +#define SSCR2_SDFD BIT(14) +#define SSCR2_SDPM BIT(16) +#define SSCR2_LJDFD BIT(17) +#define SSCR2_MMRATF BIT(18) +#define SSCR2_SMTATF BIT(19) + +/* SSR bits */ +#define SSSR_TNF BIT(2) +#define SSSR_RNE BIT(3) +#define SSSR_BSY BIT(4) +#define SSSR_TFS BIT(5) +#define SSSR_RFS BIT(6) +#define SSSR_ROR BIT(7) +#define SSSR_TUR BIT(21) + +/* SSPSP bits */ +#define SSPSP_SCMODE(x) SET_BITS(1, 0, x) +#define SSPSP_SFRMP(x) SET_BIT(2, x) +#define SSPSP_ETDS BIT(3) +#define SSPSP_STRTDLY(x) SET_BITS(6, 4, x) +#define SSPSP_DMYSTRT(x) SET_BITS(8, 7, x) +#define SSPSP_SFRMDLY(x) SET_BITS(15, 9, x) +#define SSPSP_SFRMWDTH(x) SET_BITS(21, 16, x) +#define SSPSP_DMYSTOP(x) SET_BITS(24, 23, x) +#define SSPSP_DMYSTOP_BITS 2 +#define SSPSP_DMYSTOP_MASK MASK(SSPSP_DMYSTOP_BITS - 1, 0) +#define SSPSP_FSRT BIT(25) +#define SSPSP_EDMYSTOP(x) SET_BITS(28, 26, x) + +#define SSPSP2 0x44 +#define SSPSP2_FEP_MASK 0xff + +#define SSCR3 0x48 +#define SSIOC 0x4C +#define SSP_REG_MAX SSIOC + +/* SSTSA bits */ +#define SSTSA_SSTSA(x) SET_BITS(7, 0, x) +#define SSTSA_TXEN BIT(8) + +/* SSRSA bits */ +#define SSRSA_SSRSA(x) SET_BITS(7, 0, x) +#define SSRSA_RXEN BIT(8) + +/* SSCR3 bits */ +#define SSCR3_FRM_MST_EN BIT(0) +#define SSCR3_I2S_MODE_EN BIT(1) +#define SSCR3_I2S_FRM_POL(x) SET_BIT(2, x) +#define SSCR3_I2S_TX_SS_FIX_EN BIT(3) +#define SSCR3_I2S_RX_SS_FIX_EN BIT(4) +#define SSCR3_I2S_TX_EN BIT(9) +#define SSCR3_I2S_RX_EN BIT(10) +#define SSCR3_CLK_EDGE_SEL BIT(12) +#define SSCR3_STRETCH_TX BIT(14) +#define SSCR3_STRETCH_RX BIT(15) +#define SSCR3_MST_CLK_EN BIT(16) +#define SSCR3_SYN_FIX_EN BIT(17) + +/* SSCR4 bits */ +#define SSCR4_TOT_FRM_PRD(x) ((x) << 7) + +/* SSCR5 bits */ +#define SSCR5_FRM_ASRT_CLOCKS(x) (((x) - 1) << 1) +#define SSCR5_FRM_POLARITY(x) SET_BIT(0, x) + +/* SFIFOTT bits */ +#define SFIFOTT_TX(x) ((x) - 1) +#define SFIFOTT_RX(x) (((x) - 1) << 16) + +/* SFIFOL bits */ +#define SFIFOL_TFL(x) ((x) & 0xFFFF) +#define SFIFOL_RFL(x) ((x) >> 16) + +#define SSTSA_TSEN BIT(8) +#define SSRSA_RSEN BIT(8) + +#define SSCR3_TFL_MASK MASK(5, 0) +#define SSCR3_RFL_MASK MASK(13, 8) +#define SSCR3_TFL_VAL(scr3_val) (((scr3_val) >> 0) & MASK(5, 0)) +#define SSCR3_RFL_VAL(scr3_val) (((scr3_val) >> 8) & MASK(5, 0)) +#define SSCR3_TX(x) SET_BITS(21, 16, (x) - 1) +#define SSCR3_RX(x) SET_BITS(29, 24, (x) - 1) + +#define SSIOC_TXDPDEB BIT(1) +#define SSIOC_SFCR BIT(4) +#define SSIOC_SCOE BIT(5) + +#define MAX_SSP_COUNT 8 +#define SSP_FIFO_DEPTH 16 +#define SSP_FIFO_WATERMARK 8 + +#define SSP_INTEL_QUIRK_TINTE (1 << 0) +#define SSP_INTEL_QUIRK_PINTE (1 << 1) +#define SSP_INTEL_QUIRK_SMTATF (1 << 2) +#define SSP_INTEL_QUIRK_MMRATF (1 << 3) +#define SSP_INTEL_QUIRK_PSPSTWFDFD (1 << 4) +#define SSP_INTEL_QUIRK_PSPSRWFDFD (1 << 5) +#define SSP_INTEL_QUIRK_LBM (1 << 6) +#define SSP_INTEL_QUIRK_BT_SIDEBAND (1 << 7) +#define SSP_INTEL_QUIRK_RENDER_FEEDBACK (1 << 8) + +#define SSP_INTEL_FRAME_PULSE_WIDTH_MAX 38 +#define SSP_INTEL_SLOT_PADDING_MAX 31 + +/* SSP clocks control settings */ +#define SSP_INTEL_MCLK_0_DISABLE BIT(0) +#define SSP_INTEL_MCLK_1_DISABLE BIT(1) +#define SSP_INTEL_CLKCTRL_MCLK_KA BIT(2) +#define SSP_INTEL_CLKCTRL_BCLK_KA BIT(3) +#define SSP_INTEL_CLKCTRL_FS_KA BIT(4) +#define SSP_INTEL_CLKCTRL_BCLK_IDLE_HIGH BIT(5) + +#endif /* __SSP_MACROS_H */ diff --git a/topology/nhlt/intel/ssp/ssp-process.c b/topology/nhlt/intel/ssp/ssp-process.c new file mode 100644 index 0000000..bffbc5a --- /dev/null +++ b/topology/nhlt/intel/ssp/ssp-process.c @@ -0,0 +1,1341 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Liam Girdwood +// Keyon Jie +// Rander Wang +// Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include "../intel-nhlt.h" +#include "../../nhlt.h" +#include "ssp-process.h" +#include "ssp-intel.h" +#include "ssp-internal.h" +#include "ssp-debug.h" + +static int popcount(uint32_t value) +{ + int bits_set = 0; + + while (value) { + bits_set += value & 1; + value >>= 1; + } + + return bits_set; +} + +static void ssp_calculate_intern_v30(struct intel_nhlt_params *nhlt, int hwi) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + struct ssp_intel_config_data_3_0 *blob30 = &ssp->ssp_blob_3_0[di][hwi]; + struct ssp_intel_config_data *blob = &ssp->ssp_blob[di][hwi]; + int i; + + blob30->gateway_attributes = blob->gateway_attributes; + blob30->version = SSP_BLOB_VER_3_0; + for (i = 0; i < SSP_TS_GROUP_SIZE; i++) + blob30->ts_group[i] = blob->ts_group[i]; + + blob30->ssc0 = blob->ssc0; + blob30->ssc1 = blob->ssc1; + blob30->sscto = blob->sscto; + blob30->sspsp = blob->sspsp; + blob30->ssc2 = blob->ssc2; + blob30->sspsp2 = blob->sspsp2; + blob30->rsvd2 = 0; + blob30->ssioc = blob->ssioc; + + blob30->rx_dir[0].ssmidytsa = ssp->ssp_prm[di].hw_cfg[hwi].rx_slots; + for (i = 1; i < I2SIPCMC; i++) + blob30->rx_dir[i].ssmidytsa = 0; + + blob30->tx_dir[0].ssmodytsa = ssp->ssp_prm[di].hw_cfg[hwi].tx_slots; + for (i = 1; i < I2SOPCMC; i++) + blob30->tx_dir[i].ssmodytsa = 0; + + /* mdivr count is always 1 */ + blob30->mdivctlr = blob->mdivc; + ssp->ssp_prm[di].mdivr[hwi].count = 1; + blob30->mdivrcnt = ssp->ssp_prm[di].mdivr[hwi].count; + ssp->ssp_prm[di].mdivr[hwi].mdivrs[0] = blob->mdivr; + + blob30->size = sizeof(struct ssp_intel_config_data_3_0) + + blob30->mdivrcnt * sizeof(uint32_t) + + ssp->ssp_blob_ext[di][hwi].size; +} + +static void ssp_calculate_intern_v15(struct intel_nhlt_params *nhlt, int hwi) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + struct ssp_intel_config_data_1_5 *blob15 = &ssp->ssp_blob_1_5[di][hwi]; + struct ssp_intel_config_data *blob = &ssp->ssp_blob[di][hwi]; + int i; + + blob15->gateway_attributes = ssp->ssp_blob[di][hwi].gateway_attributes; + blob15->version = SSP_BLOB_VER_1_5; + + for (i = 0; i < SSP_TS_GROUP_SIZE; i++) + blob15->ts_group[i] = blob->ts_group[i]; + + blob15->ssc0 = blob->ssc0; + blob15->ssc1 = blob->ssc1; + blob15->sscto = blob->sscto; + blob15->sspsp = blob->sspsp; + blob15->sstsa = blob->sstsa; + blob15->ssrsa = blob->ssrsa; + blob15->ssc2 = blob->ssc2; + blob15->sspsp2 = blob->sspsp2; + blob15->ssc3 = blob->ssc3; + blob15->ssioc = blob->ssioc; + + /* for now we use only 1 divider as in legacy */ + blob15->mdivctlr = blob->mdivc; + ssp->ssp_prm[di].mdivr[hwi].count = 1; + blob15->mdivrcnt = ssp->ssp_prm[di].mdivr[hwi].count; + ssp->ssp_prm[di].mdivr[hwi].mdivrs[0] = blob->mdivr; + + blob15->size = sizeof(struct ssp_intel_config_data_1_5) + + blob15->mdivrcnt * sizeof(uint32_t) + + ssp->ssp_blob_ext[di][hwi].size; +} + +static int ssp_calculate_intern(struct intel_nhlt_params *nhlt, int hwi) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + uint32_t active_tx_slots = 2; + uint32_t active_rx_slots = 2; + uint32_t inverted_frame = 0; + uint32_t inverted_bclk = 0; + uint32_t frame_end_padding; + uint32_t total_sample_size; + uint32_t slot_end_padding; + bool start_delay = false; + uint32_t frame_len = 0; + uint32_t sample_width; + uint32_t end_padding; + uint32_t data_size; + uint32_t bdiv_min; + bool cfs = false; + uint32_t clk_div; + uint32_t bdiv; + uint32_t tft; + uint32_t rft; + int di; + unsigned int i, j; + + if (!ssp) + return -EINVAL; + + di = ssp->ssp_count; + + /* should be eventually the lp_mode defined in pipeline */ + ssp->ssp_blob[di][hwi].gateway_attributes = 0; + + for (j = 0; j < SSP_TDM_MAX_SLOT_MAP_COUNT; j++) { + for (i = 0; i < ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots; i++) + ssp->ssp_blob[di][hwi].ts_group[j] |= (i << (i * 4)); + for (; i < SSP_TDM_MAX_SLOT_MAP_COUNT; i++) + ssp->ssp_blob[di][hwi].ts_group[j] |= (0xF << (i * 4)); + } + + /* reset SSP settings */ + /* sscr0 dynamic settings are DSS, EDSS, SCR, FRDC, ECS */ + ssp->ssp_blob[di][hwi].ssc0 = SSCR0_MOD | SSCR0_PSP | SSCR0_RIM | SSCR0_TIM; + + /* sscr1 dynamic settings are SFRMDIR, SCLKDIR, SCFR */ + if (ssp->ssp_prm[di].version == SSP_BLOB_VER_3_0) + /* bits 21 and 20, TSRE and RSRE do not exist in ACE3.x + * Note: Assuming SSP_BLOB_VER_3_0 is ACE3.x + */ + ssp->ssp_blob[di][hwi].ssc1 = SSCR1_TTE | SSCR1_TTELP | SSCR1_TRAIL; + else + ssp->ssp_blob[di][hwi].ssc1 = SSCR1_TTE | SSCR1_TTELP | SSCR1_TRAIL | + SSCR1_RSRE | SSCR1_TSRE; + + /* sscr2 dynamic setting is LJDFD */ + ssp->ssp_blob[di][hwi].ssc2 = SSCR2_SDFD | SSCR2_TURM1; + + /* sscr3 dynamic settings are TFT, RFT */ + ssp->ssp_blob[di][hwi].ssc3 = 0; + + /* sspsp dynamic settings are SCMODE, SFRMP, DMYSTRT, SFRMWDTH */ + ssp->ssp_blob[di][hwi].sspsp = 0; + + /* sspsp2 no dynamic setting */ + ssp->ssp_blob[di][hwi].sspsp2 = 0x0; + + /* ssioc dynamic setting is SFCR */ + ssp->ssp_blob[di][hwi].ssioc = SSIOC_SCOE; + + /* ssto no dynamic setting */ + ssp->ssp_blob[di][hwi].sscto = 0x0; + + /* sstsa dynamic setting is TTSA, default 2 slots */ + ssp->ssp_blob[di][hwi].sstsa = SSTSA_SSTSA(ssp->ssp_prm[di].hw_cfg[hwi].tx_slots); + + /* ssrsa dynamic setting is RTSA, default 2 slots */ + ssp->ssp_blob[di][hwi].ssrsa = SSRSA_SSRSA(ssp->ssp_prm[di].hw_cfg[hwi].rx_slots); + + switch (ssp->ssp_prm[di].hw_cfg[hwi].format & SSP_FMT_CLOCK_PROVIDER_MASK) { + case SSP_FMT_CBP_CFP: + ssp->ssp_blob[di][hwi].ssc1 |= SSCR1_SCLKDIR | SSCR1_SFRMDIR; + break; + case SSP_FMT_CBC_CFC: + ssp->ssp_blob[di][hwi].ssc1 |= SSCR1_SCFR; + cfs = true; + break; + case SSP_FMT_CBP_CFC: + ssp->ssp_blob[di][hwi].ssc1 |= SSCR1_SCLKDIR; + /* FIXME: this mode has not been tested */ + + cfs = true; + break; + case SSP_FMT_CBC_CFP: + ssp->ssp_blob[di][hwi].ssc1 |= SSCR1_SCFR | SSCR1_SFRMDIR; + /* FIXME: this mode has not been tested */ + break; + default: + fprintf(stderr, "ssp_calculate(): format & PROVIDER_MASK EINVAL\n"); + return -EINVAL; + } + + /* clock signal polarity */ + switch (ssp->ssp_prm[di].hw_cfg[hwi].format & SSP_FMT_INV_MASK) { + case SSP_FMT_NB_NF: + break; + case SSP_FMT_NB_IF: + inverted_frame = 1; /* handled later with format */ + break; + case SSP_FMT_IB_IF: + inverted_bclk = 1; /* handled later with bclk idle */ + inverted_frame = 1; /* handled later with format */ + break; + case SSP_FMT_IB_NF: + inverted_bclk = 1; /* handled later with bclk idle */ + break; + default: + fprintf(stderr, "ssp_calculate: format & INV_MASK EINVAL\n"); + return -EINVAL; + } + + /* supporting bclk idle state */ + if (ssp->ssp_prm[di].clks_control & + SSP_INTEL_CLKCTRL_BCLK_IDLE_HIGH) { + /* bclk idle state high */ + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_SCMODE((inverted_bclk ^ 0x3) & 0x3); + } else { + /* bclk idle state low */ + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_SCMODE(inverted_bclk); + } + + /* Note: ACS as SSCR0(30) does not exist in any ACE version, or cAVS2.x. This disables + * it for ACE3.x. It might be good to fix later for other platforms. In cAVS this is 30:29 + * reserved, in ACE1.x this is DLE as 30:29, in ACE2.x this is RSVD30 as 30:29, in ACE3.x this + * is DLE as 30:29. + */ + if (ssp->ssp_prm[di].version != SSP_BLOB_VER_3_0) + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_ACS; + + /* Additional hardware settings */ + + /* Receiver Time-out Interrupt Disabled/Enabled */ + ssp->ssp_blob[di][hwi].ssc1 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_TINTE) ? + SSCR1_TINTE : 0; + + /* Peripheral Trailing Byte Interrupts Disable/Enable */ + ssp->ssp_blob[di][hwi].ssc1 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_PINTE) ? + SSCR1_PINTE : 0; + + /* Enable/disable internal loopback. Output of transmit serial + * shifter connected to input of receive serial shifter, internally. + */ + ssp->ssp_blob[di][hwi].ssc1 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_LBM) ? + SSCR1_LBM : 0; + + /* Transmit data are driven at the same/opposite clock edge specified + * in SSPSP.SCMODE[1:0] + */ + ssp->ssp_blob[di][hwi].ssc2 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_SMTATF) ? + SSCR2_SMTATF : 0; + + /* Receive data are sampled at the same/opposite clock edge specified + * in SSPSP.SCMODE[1:0] + */ + ssp->ssp_blob[di][hwi].ssc2 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_MMRATF) ? + SSCR2_MMRATF : 0; + + /* Enable/disable the fix for PSP consumer mode TXD wait for frame + * de-assertion before starting the second channel + */ + ssp->ssp_blob[di][hwi].ssc2 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_PSPSTWFDFD) ? + SSCR2_PSPSTWFDFD : 0; + + /* Enable/disable the fix for PSP provider mode FSRT with dummy stop & + * frame end padding capability + */ + ssp->ssp_blob[di][hwi].ssc2 |= (ssp->ssp_prm[di].quirks & SSP_INTEL_QUIRK_PSPSRWFDFD) ? + SSCR2_PSPSRWFDFD : 0; + + if (!ssp->ssp_prm[di].hw_cfg[hwi].mclk_rate) { + fprintf(stderr, "ssp_calculate(): invalid MCLK = %u \n", + ssp->ssp_prm[di].hw_cfg[hwi].mclk_rate); + return -EINVAL; + } + + if (!ssp->ssp_prm[di].hw_cfg[hwi].bclk_rate || + ssp->ssp_prm[di].hw_cfg[hwi].bclk_rate > ssp->ssp_prm[di].hw_cfg[hwi].mclk_rate) { + fprintf(stderr, "ssp_calculate(): BCLK %u Hz = 0 or > MCLK %u Hz\n", + ssp->ssp_prm[di].hw_cfg[hwi].bclk_rate, + ssp->ssp_prm[di].hw_cfg[hwi].mclk_rate); + return -EINVAL; + } + + /* calc frame width based on BCLK and rate - must be divisible */ + if (ssp->ssp_prm[di].hw_cfg[hwi].bclk_rate % ssp->ssp_prm[di].hw_cfg[hwi].fsync_rate) { + fprintf(stderr, "ssp_calculate(): BCLK %u is not divisible by rate %u\n", + ssp->ssp_prm[di].hw_cfg[hwi].bclk_rate, + ssp->ssp_prm[di].hw_cfg[hwi].fsync_rate); + return -EINVAL; + } + + /* must be enough BCLKs for data */ + bdiv = ssp->ssp_prm[di].hw_cfg[hwi].bclk_rate / ssp->ssp_prm[di].hw_cfg[hwi].fsync_rate; + if (bdiv < ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width * + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots) { + fprintf(stderr, "ssp_calculate(): not enough BCLKs need %u\n", + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width * + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots); + return -EINVAL; + } + + /* tdm_slot_width must be <= 38 for SSP */ + if (ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width > 38) { + fprintf(stderr, "ssp_calculate(): tdm_slot_width %u > 38\n", + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width); + return -EINVAL; + } + + bdiv_min = ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots * + (ssp->ssp_prm[di].tdm_per_slot_padding_flag ? + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width : + ssp->ssp_prm[di].sample_valid_bits); + if (bdiv < bdiv_min) { + fprintf(stderr, "ssp_calculate(): bdiv(%u) < bdiv_min(%u)\n", + bdiv, bdiv_min); + return -EINVAL; + } + + frame_end_padding = bdiv - bdiv_min; + if (frame_end_padding > SSPSP2_FEP_MASK) { + fprintf(stderr, "ssp_calculate(): frame_end_padding too big: %u\n", + frame_end_padding); + return -EINVAL; + } + + /* format */ + switch (ssp->ssp_prm[di].hw_cfg[hwi].format & SSP_FMT_FORMAT_MASK) { + case SSP_FMT_I2S: + + start_delay = true; + + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_FRDC(ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots); + + if (bdiv % 2) { + fprintf(stderr, "ssp_calculate(): bdiv %u is not divisible by 2\n", + bdiv); + return -EINVAL; + } + + /* set asserted frame length to half frame length */ + frame_len = bdiv / 2; + + /* + * handle frame polarity, I2S default is falling/active low, + * non-inverted(inverted_frame=0) -- active low(SFRMP=0), + * inverted(inverted_frame=1) -- rising/active high(SFRMP=1), + * so, we should set SFRMP to inverted_frame. + */ + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_SFRMP(inverted_frame); + + /* + * for I2S/LEFT_J, the padding has to happen at the end + * of each slot + */ + if (frame_end_padding % 2) { + fprintf(stderr, "ssp_calculate():frame_end_padding %u not divisible by 2\n", + frame_end_padding); + return -EINVAL; + } + + slot_end_padding = frame_end_padding / 2; + + if (slot_end_padding > SSP_INTEL_SLOT_PADDING_MAX) { + /* too big padding */ + fprintf(stderr, "ssp_calculate(): slot_end_padding > %d\n", + SSP_INTEL_SLOT_PADDING_MAX); + return -EINVAL; + } + + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_DMYSTOP(slot_end_padding); + slot_end_padding >>= SSPSP_DMYSTOP_BITS; + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_EDMYSTOP(slot_end_padding); + + break; + + case SSP_FMT_LEFT_J: + + /* default start_delay value is set to false */ + + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_FRDC(ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots); + + /* LJDFD enable */ + ssp->ssp_blob[di][hwi].ssc2 &= ~SSCR2_LJDFD; + + if (bdiv % 2) { + fprintf(stderr, "ssp_calculate(): bdiv %u is not divisible by 2\n", + bdiv); + return -EINVAL; + } + + /* set asserted frame length to half frame length */ + frame_len = bdiv / 2; + + /* + * handle frame polarity, LEFT_J default is rising/active high, + * non-inverted(inverted_frame=0) -- active high(SFRMP=1), + * inverted(inverted_frame=1) -- falling/active low(SFRMP=0), + * so, we should set SFRMP to !inverted_frame. + */ + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_SFRMP(!inverted_frame ? 1 : 0); + + /* + * for I2S/LEFT_J, the padding has to happen at the end + * of each slot + */ + if (frame_end_padding % 2) { + fprintf(stderr, "ssp_set_config(): frame padding %u not divisible by 2\n", + frame_end_padding); + return -EINVAL; + } + + slot_end_padding = frame_end_padding / 2; + + if (slot_end_padding > 15) { + /* can't handle padding over 15 bits */ + fprintf(stderr, "ssp_set_config(): slot_end_padding %u > 15 bits\n", + slot_end_padding); + return -EINVAL; + } + + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_DMYSTOP(slot_end_padding); + slot_end_padding >>= SSPSP_DMYSTOP_BITS; + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_EDMYSTOP(slot_end_padding); + + break; + case SSP_FMT_DSP_A: + + start_delay = true; + + /* fallthrough */ + + case SSP_FMT_DSP_B: + + /* default start_delay value is set to false */ + + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_MOD | + SSCR0_FRDC(ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots); + + /* set asserted frame length */ + frame_len = 1; /* default */ + + if (cfs && ssp->ssp_prm[di].frame_pulse_width > 0 && + ssp->ssp_prm[di].frame_pulse_width <= + SSP_INTEL_FRAME_PULSE_WIDTH_MAX) { + frame_len = ssp->ssp_prm[di].frame_pulse_width; + } + + /* frame_pulse_width must less or equal 38 */ + if (ssp->ssp_prm[di].frame_pulse_width > + SSP_INTEL_FRAME_PULSE_WIDTH_MAX) { + fprintf(stderr, "ssp_set_config(): frame_pulse_width > %d\n", + SSP_INTEL_FRAME_PULSE_WIDTH_MAX); + return -EINVAL; + } + /* + * handle frame polarity, DSP_B default is rising/active high, + * non-inverted(inverted_frame=0) -- active high(SFRMP=1), + * inverted(inverted_frame=1) -- falling/active low(SFRMP=0), + * so, we should set SFRMP to !inverted_frame. + */ + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_SFRMP(!inverted_frame ? 1 : 0); + + active_tx_slots = popcount(ssp->ssp_prm[di].hw_cfg[hwi].tx_slots); + active_rx_slots = popcount(ssp->ssp_prm[di].hw_cfg[hwi].rx_slots); + + /* + * handle TDM mode, TDM mode has padding at the end of + * each slot. The amount of padding is equal to result of + * subtracting slot width and valid bits per slot. + */ + if (ssp->ssp_prm[di].tdm_per_slot_padding_flag) { + frame_end_padding = bdiv - ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots * + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width; + + slot_end_padding = ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width - + ssp->ssp_prm[di].sample_valid_bits; + + if (slot_end_padding > + SSP_INTEL_SLOT_PADDING_MAX) { + fprintf(stderr, "ssp_set_config(): slot_end_padding > %d\n", + SSP_INTEL_SLOT_PADDING_MAX); + return -EINVAL; + } + + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_DMYSTOP(slot_end_padding); + slot_end_padding >>= SSPSP_DMYSTOP_BITS; + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_EDMYSTOP(slot_end_padding); + } + + ssp->ssp_blob[di][hwi].sspsp2 |= (frame_end_padding & SSPSP2_FEP_MASK); + + break; + default: + fprintf(stderr, "ssp_set_config(): invalid format 0x%04x\n", + ssp->ssp_prm[di].hw_cfg[hwi].format); + return -EINVAL; + } + + if (start_delay) + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_FSRT; + + ssp->ssp_blob[di][hwi].sspsp |= SSPSP_SFRMWDTH(frame_len); + + data_size = ssp->ssp_prm[di].sample_valid_bits; + + if (data_size > 16) + ssp->ssp_blob[di][hwi].ssc0 |= (SSCR0_EDSS | SSCR0_DSIZE(data_size - 16)); + else + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_DSIZE(data_size); + + end_padding = 0; + total_sample_size = ssp->ssp_prm[di].hw_cfg[hwi].tdm_slot_width * + ssp->ssp_prm[di].hw_cfg[hwi].tdm_slots; + while (ssp->ssp_prm[di].io_clk % ((total_sample_size + end_padding) * + ssp->ssp_prm[di].hw_cfg[hwi].fsync_rate)) { + if (++end_padding >= 256) + break; + } + + if (end_padding >= 256) + return -EINVAL; + + /* calc scr divisor */ + clk_div = ssp->ssp_prm[di].io_clk / ((total_sample_size + end_padding) * + ssp->ssp_prm[di].hw_cfg[hwi].fsync_rate); + if (clk_div >= 4095) + return -EINVAL; + + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_SCR(clk_div - 1); + + /* setting TFT and RFT */ + switch (ssp->ssp_prm[di].sample_valid_bits) { + case 16: + /* use 2 bytes for each slot */ + sample_width = 2; + break; + case 24: + case 32: + /* use 4 bytes for each slot */ + sample_width = 4; + break; + default: + fprintf(stderr, "ssp_set_config(): sample_valid_bits %u\n", + ssp->ssp_prm[di].sample_valid_bits); + return -EINVAL; + } + + tft = MIN((uint32_t)(SSP_FIFO_DEPTH - SSP_FIFO_WATERMARK), + sample_width * active_tx_slots); + rft = MIN((uint32_t)(SSP_FIFO_DEPTH - SSP_FIFO_WATERMARK), + sample_width * active_rx_slots); + + ssp->ssp_blob[di][hwi].ssc3 |= SSCR3_TX(tft) | SSCR3_RX(rft); + + /* calc mn divisor */ + if (ssp->ssp_prm[di].io_clk % ssp->ssp_prm[di].hw_cfg[hwi].mclk_rate) { + fprintf(stderr, "ssp_set_config(): io_clk not divisible with mclk\n"); + return -EINVAL; + } + + clk_div = ssp->ssp_prm[di].io_clk / ssp->ssp_prm[di].hw_cfg[hwi].mclk_rate; + if (clk_div > 1) + clk_div -= 2; + else + clk_div = 0xFFF; /* bypass clk divider */ + + ssp->ssp_blob[di][hwi].mdivr = clk_div; + /* clock will always go through the divider */ + + /* Note: There is no SSC0(6) ECS in ACE3.x but RSVD6 in same bit position + * that must be set to one. + */ + ssp->ssp_blob[di][hwi].ssc0 |= SSCR0_ECS; + + /* enable divider for this clock id */ + ssp->ssp_blob[di][hwi].mdivc |= BIT(ssp->ssp_prm[di].mclk_id); + /* set mclk source always for audio cardinal clock */ + ssp->ssp_blob[di][hwi].mdivc |= MCDSS(SSP_CLOCK_AUDIO_CARDINAL); + /* set bclk source for audio cardinal clock + * Note: There is no MDIVXCTRL(21:20) MNDSS in any ACE version 1.x - 3.x. MNDSS + * exists in cAVS2.x. This removes the set for ACE3.x. May need to address other + * ACE platforms later. + */ + if (ssp->ssp_prm[di].version != SSP_BLOB_VER_3_0) + ssp->ssp_blob[di][hwi].mdivc |= MNDSS(SSP_CLOCK_AUDIO_CARDINAL); + + return 0; +} + +static int ssp_calculate_intern_ext(struct intel_nhlt_params *nhlt, int hwi) +{ + size_t aux_size, mn_size, clk_size, tr_size, run_size, sync_size, node_size, ext_size, + link_size, size, total_size; + struct intel_ssp_params *ssp; + struct ssp_config_aux *aux; + struct ssp_intel_aux_tlv *tlv; + struct ssp_intel_mn_ctl *mn; + struct ssp_intel_clk_ctl *clk; + struct ssp_intel_tr_ctl *tr; + struct ssp_intel_run_ctl *run; + struct ssp_intel_sync_ctl *sync; + struct ssp_intel_node_ctl *node; + struct ssp_intel_ext_ctl *ext; + struct ssp_intel_link_ctl *link; + uint8_t *aux_blob; + uint32_t enabled; + unsigned int i; + int di; + + aux_size = sizeof(struct ssp_intel_aux_tlv); + mn_size = sizeof(struct ssp_intel_mn_ctl); + clk_size = sizeof(struct ssp_intel_clk_ctl); + tr_size = sizeof(struct ssp_intel_tr_ctl); + run_size = sizeof(struct ssp_intel_run_ctl); + sync_size = sizeof(struct ssp_intel_sync_ctl); + node_size = sizeof(struct ssp_intel_node_ctl); + ext_size = sizeof(struct ssp_intel_ext_ctl); + link_size = sizeof(struct ssp_intel_link_ctl); + + ssp = (struct intel_ssp_params *)nhlt->ssp_params; + di = ssp->ssp_count; + enabled = ssp->ssp_prm[di].aux_cfg[hwi].enabled; + aux = &(ssp->ssp_prm[di].aux_cfg[hwi]); + aux_blob = ssp->ssp_blob_ext[di][hwi].aux_blob; + total_size = 0; + size = 0; + + if (enabled & BIT(SSP_MN_DIVIDER_CONTROLS)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + mn = (struct ssp_intel_mn_ctl *)(aux_blob + aux_size); + size = mn_size + aux_size; + tlv->type = SSP_MN_DIVIDER_CONTROLS; + tlv->size = mn_size; + mn->div_m = aux->mn.m_div; + mn->div_n = aux->mn.n_div; + aux_blob += size; + total_size += size; + } + + if (enabled & BIT(SSP_DMA_CLK_CONTROLS)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + clk = (struct ssp_intel_clk_ctl *)(aux_blob + aux_size); + size = clk_size + aux_size; + tlv->type = SSP_DMA_CLK_CONTROLS; + tlv->size = clk_size; + clk->start |= SET_BITS(15, 0, aux->clk.clock_warm_up); + clk->start |= SET_BIT(16, aux->clk.mclk); + clk->start |= SET_BIT(17, aux->clk.warm_up_ovr); + clk->stop |= SET_BITS(15, 0, aux->clk.clock_stop_delay); + clk->stop |= SET_BIT(16, aux->clk.keep_running); + clk->stop |= SET_BIT(17, aux->clk.clock_stop_ovr); + aux_blob += size; + total_size += size; + } + + if (enabled & BIT(SSP_DMA_TRANSMISSION_START)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + tr = (struct ssp_intel_tr_ctl *)(aux_blob + aux_size); + size = tr_size + aux_size; + tlv->type = SSP_DMA_TRANSMISSION_START; + tlv->size = tr_size; + tr->sampling_frequency = aux->tr_start.sampling_frequency; + tr->bit_depth = aux->tr_start.bit_depth; + tr->channel_map = aux->tr_start.channel_map; + tr->channel_config = aux->tr_start.channel_config; + tr->interleaving_style = aux->tr_start.interleaving_style; + tr->format |= SET_BITS(7, 0, aux->tr_start.number_of_channels); + tr->format |= SET_BITS(15, 8, aux->tr_start.valid_bit_depth); + tr->format |= SET_BITS(23, 16, aux->tr_start.sample_type); + aux_blob += size; + total_size += size; + } + + if (enabled & BIT(SSP_DMA_TRANSMISSION_STOP)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + tr = (struct ssp_intel_tr_ctl *)(aux_blob + aux_size); + size = tr_size + aux_size; + tlv->type = SSP_DMA_TRANSMISSION_STOP; + tlv->size = tr_size; + tr->sampling_frequency = aux->tr_stop.sampling_frequency; + tr->bit_depth = aux->tr_stop.bit_depth; + tr->channel_map = aux->tr_stop.channel_map; + tr->channel_config = aux->tr_stop.channel_config; + tr->interleaving_style = aux->tr_stop.interleaving_style; + tr->format |= SET_BITS(7, 0, aux->tr_stop.number_of_channels); + tr->format |= SET_BITS(15, 8, aux->tr_stop.valid_bit_depth); + tr->format |= SET_BITS(23, 16, aux->tr_stop.sample_type); + aux_blob += size; + total_size += size; + } + + if (enabled & BIT(SSP_DMA_ALWAYS_RUNNING_MODE)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + run = (struct ssp_intel_run_ctl *)(aux_blob + aux_size); + size = run_size + aux_size; + tlv->type = SSP_DMA_ALWAYS_RUNNING_MODE; + tlv->size = run_size; + run->enabled = aux->run.always_run; + aux_blob += size; + total_size += size; + } + + if (enabled & BIT(SSP_DMA_SYNC_DATA)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + sync = (struct ssp_intel_sync_ctl *)(aux_blob + aux_size); + size = sync_size + aux_size; + tlv->type = SSP_DMA_SYNC_DATA; + tlv->size = sync_size; + sync->sync_denominator = aux->sync.sync_denominator; + sync->count = aux->sync.count; + aux_blob += size; + total_size += size; + for (i = 0; i < sync->count; i++) { + node = (struct ssp_intel_node_ctl *)(aux_blob); + size = node_size; + node->node_id = aux->sync.nodes[i].node_id; + node->sampling_rate = aux->sync.nodes[i].sampling_rate; + tlv->size += node_size; + aux_blob += size; + total_size += size; + } + } + + if (enabled & BIT(SSP_DMA_CLK_CONTROLS_EXT)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + ext = (struct ssp_intel_ext_ctl *)(aux_blob + aux_size); + size = ext_size + aux_size; + tlv->type = SSP_DMA_CLK_CONTROLS_EXT; + tlv->size = ext_size; + ext->ext_data |= SET_BIT(0, aux->ext.mclk_policy_override); + ext->ext_data |= SET_BIT(1, aux->ext.mclk_always_running); + ext->ext_data |= SET_BIT(2, aux->ext.mclk_starts_on_gtw_init); + ext->ext_data |= SET_BIT(3, aux->ext.mclk_starts_on_run); + ext->ext_data |= SET_BIT(4, aux->ext.mclk_starts_on_pause); + ext->ext_data |= SET_BIT(5, aux->ext.mclk_stops_on_pause); + ext->ext_data |= SET_BIT(6, aux->ext.mclk_stops_on_reset); + ext->ext_data |= SET_BIT(8, aux->ext.bclk_policy_override); + ext->ext_data |= SET_BIT(9, aux->ext.bclk_always_running); + ext->ext_data |= SET_BIT(10, aux->ext.bclk_starts_on_gtw_init); + ext->ext_data |= SET_BIT(11, aux->ext.bclk_starts_on_run); + ext->ext_data |= SET_BIT(12, aux->ext.bclk_starts_on_pause); + ext->ext_data |= SET_BIT(13, aux->ext.bclk_stops_on_pause); + ext->ext_data |= SET_BIT(14, aux->ext.bclk_stops_on_reset); + ext->ext_data |= SET_BIT(16, aux->ext.sync_policy_override); + ext->ext_data |= SET_BIT(17, aux->ext.sync_always_running); + ext->ext_data |= SET_BIT(18, aux->ext.sync_starts_on_gtw_init); + ext->ext_data |= SET_BIT(19, aux->ext.sync_starts_on_run); + ext->ext_data |= SET_BIT(20, aux->ext.sync_starts_on_pause); + ext->ext_data |= SET_BIT(21, aux->ext.sync_stops_on_pause); + ext->ext_data |= SET_BIT(22, aux->ext.sync_stops_on_reset); + aux_blob += size; + total_size += size; + } + + if (enabled & BIT(SSP_LINK_CLK_SOURCE)) { + tlv = (struct ssp_intel_aux_tlv *)aux_blob; + link = (struct ssp_intel_link_ctl *)(aux_blob + aux_size); + size = link_size + aux_size; + tlv->type = SSP_LINK_CLK_SOURCE; + tlv->size = link_size; + link->clock_source = aux->link.clock_source; + aux_blob += size; + total_size += size; + } + + ssp->ssp_blob_ext[di][hwi].size = total_size; + + return 0; +} + + + +int ssp_calculate(struct intel_nhlt_params *nhlt) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + unsigned int i; + + if (!ssp) + return -EINVAL; + + /* calculate blob for every hw config */ + for (i = 0; i < ssp->ssp_hw_config_count[ssp->ssp_count]; i++) { + if (ssp_calculate_intern(nhlt, i) < 0) + return -EINVAL; + if (ssp_calculate_intern_ext(nhlt, i) < 0) + return -EINVAL; + /* v15 blob is made from legacy blob, so it can't fail */ + ssp_calculate_intern_v15(nhlt, i); + + /* v30 blob is made from legacy blob, so it can't fail */ + ssp_calculate_intern_v30(nhlt, i); + } + + ssp_print_internal(ssp); + ssp_print_calculated(ssp); + + ssp->ssp_count++; + + return 0; +} + +int ssp_get_dir(struct intel_nhlt_params *nhlt, int dai_index, uint8_t *dir) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + + if (!ssp) + return -EINVAL; + + *dir = ssp->ssp_prm[dai_index].direction; + + return 0; +} + +int ssp_get_params(struct intel_nhlt_params *nhlt, int dai_index, uint32_t *virtualbus_id, + uint32_t *formats_count, uint32_t *device_type, uint32_t *direction) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + + if (!ssp) + return -EINVAL; + + *virtualbus_id = ssp->ssp_dai_index[dai_index]; + *formats_count = ssp->ssp_hw_config_count[dai_index]; + if (ssp->ssp_prm[dai_index].quirks & SSP_INTEL_QUIRK_BT_SIDEBAND) + *device_type = NHLT_DEVICE_TYPE_SSP_BT_SIDEBAND; + else + *device_type = 0; + if (ssp->ssp_prm[dai_index].quirks & SSP_INTEL_QUIRK_RENDER_FEEDBACK) { + if (*direction == NHLT_ENDPOINT_DIRECTION_RENDER) + *direction = NHLT_ENDPOINT_DIRECTION_RENDER_WITH_LOOPBACK; + else if (*direction == NHLT_ENDPOINT_DIRECTION_CAPTURE) + *direction = NHLT_ENDPOINT_DIRECTION_FEEDBACK_FOR_RENDER; + } + + return 0; +} + +int ssp_get_hw_params(struct intel_nhlt_params *nhlt, int dai_index, int hw_index, + uint32_t *sample_rate, uint16_t *channel_count, uint32_t *bits_per_sample) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + + if (!ssp) + return -EINVAL; + + *channel_count = ssp->ssp_prm[dai_index].hw_cfg[hw_index].tdm_slots; + *sample_rate = ssp->ssp_prm[dai_index].hw_cfg[hw_index].fsync_rate; + *bits_per_sample = ssp->ssp_prm[dai_index].hw_cfg[hw_index].tdm_slot_width; + + return 0; +} + +/* + * Build ssp vendor blob from calculated parameters. + * + * Supposed to be called after all ssp DAIs are parsed from topology and the final nhlt blob is + * generated. + */ +int ssp_get_vendor_blob_size(struct intel_nhlt_params *nhlt, int dai_index, + int hw_config_index, size_t *size) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + + if (!ssp) + return -EINVAL; + + /* set size for the blob */ + if (ssp->ssp_prm[dai_index].version == SSP_BLOB_VER_1_5) + *size = ssp->ssp_blob_1_5[dai_index][hw_config_index].size; + else if (ssp->ssp_prm[dai_index].version == SSP_BLOB_VER_3_0) + *size = ssp->ssp_blob_3_0[dai_index][hw_config_index].size; + else + /* legacy */ + *size = sizeof(struct ssp_intel_config_data) + + ssp->ssp_blob_ext[dai_index][hw_config_index].size; + + return 0; +} + +int ssp_get_vendor_blob_count(struct intel_nhlt_params *nhlt) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + + if (!ssp || !ssp->ssp_count) + return -EINVAL; + + return ssp->ssp_count; +} + +/* Get the size of dynamic vendor blob to reserve proper amount of memory */ +int ssp_get_vendor_blob(struct intel_nhlt_params *nhlt, uint8_t *vendor_blob, + int dai_index, int hw_config_index) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + uint32_t basic_len, clock_len; + + if (!ssp) + return -EINVAL; + + /* top level struct */ + if (ssp->ssp_prm[dai_index].version == SSP_BLOB_VER_1_5) { + basic_len = sizeof(struct ssp_intel_config_data_1_5); + clock_len = sizeof(uint32_t) * ssp->ssp_prm[dai_index].mdivr[hw_config_index].count; + /* basic data */ + memcpy(vendor_blob, &ssp->ssp_blob_1_5[dai_index][hw_config_index], basic_len); + /* clock data */ + memcpy(vendor_blob + basic_len, + &ssp->ssp_prm[dai_index].mdivr[hw_config_index].mdivrs[0], clock_len); + /* ext data */ + memcpy(vendor_blob + basic_len + clock_len, + ssp->ssp_blob_ext[dai_index][hw_config_index].aux_blob, + ssp->ssp_blob_ext[dai_index][hw_config_index].size); + } else if (ssp->ssp_prm[dai_index].version == SSP_BLOB_VER_3_0) { + basic_len = sizeof(struct ssp_intel_config_data_3_0); + clock_len = sizeof(uint32_t) * ssp->ssp_prm[dai_index].mdivr[hw_config_index].count; + /* basic data */ + memcpy(vendor_blob, &ssp->ssp_blob_3_0[dai_index][hw_config_index], basic_len); + /* clock data */ + memcpy(vendor_blob + basic_len, + &ssp->ssp_prm[dai_index].mdivr[hw_config_index].mdivrs[0], clock_len); + /* ext data */ + memcpy(vendor_blob + basic_len + clock_len, + ssp->ssp_blob_ext[dai_index][hw_config_index].aux_blob, + ssp->ssp_blob_ext[dai_index][hw_config_index].size); + } else { + basic_len = sizeof(struct ssp_intel_config_data); + /*basic data */ + memcpy(vendor_blob, &ssp->ssp_blob[dai_index][hw_config_index], basic_len); + /* ext data */ + memcpy(vendor_blob + basic_len, + ssp->ssp_blob_ext[dai_index][hw_config_index].aux_blob, + ssp->ssp_blob_ext[dai_index][hw_config_index].size); + } + + return 0; +} + +int ssp_set_params(struct intel_nhlt_params *nhlt, const char *dir, int dai_index, int io_clk, + int bclk_delay, int sample_bits, int mclk_id, int clks_control, + int frame_pulse_width, const char *tdm_padding_per_slot, const char *quirks, + int version) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + char delim[] = ","; + char *buf, *token = NULL; + + if (!ssp) + return -EINVAL; + + if (dir) { + if (!strcmp(dir, "playback")) + ssp->ssp_prm[ssp->ssp_count].direction = NHLT_ENDPOINT_DIRECTION_RENDER; + else if (!strcmp(dir, "capture")) + ssp->ssp_prm[ssp->ssp_count].direction = NHLT_ENDPOINT_DIRECTION_CAPTURE; + else if (!strcmp(dir, "duplex")) + ssp->ssp_prm[ssp->ssp_count].direction = + NHLT_ENDPOINT_DIRECTION_FEEDBACK_FOR_RENDER + 1; + else + return -EINVAL; + } + ssp->ssp_dai_index[ssp->ssp_count] = dai_index; + ssp->ssp_prm[ssp->ssp_count].io_clk = io_clk; + ssp->ssp_prm[ssp->ssp_count].bclk_delay = bclk_delay; + ssp->ssp_prm[ssp->ssp_count].sample_valid_bits = sample_bits; + ssp->ssp_prm[ssp->ssp_count].mclk_id = mclk_id; + ssp->ssp_prm[ssp->ssp_count].clks_control = clks_control; + ssp->ssp_prm[ssp->ssp_count].frame_pulse_width = frame_pulse_width; + /* let's compare the lower 16 bits as we don't send the signature from topology */ + if (version == (SSP_BLOB_VER_1_5 & ((1 << 16) - 1))) + ssp->ssp_prm[ssp->ssp_count].version = SSP_BLOB_VER_1_5; + else if (version == (SSP_BLOB_VER_3_0 & ((1 << 16) - 1))) + ssp->ssp_prm[ssp->ssp_count].version = SSP_BLOB_VER_3_0; + + if (tdm_padding_per_slot && !strcmp(tdm_padding_per_slot, "true")) + ssp->ssp_prm[ssp->ssp_count].tdm_per_slot_padding_flag = 1; + else + ssp->ssp_prm[ssp->ssp_count].tdm_per_slot_padding_flag = 0; + + ssp->ssp_prm[ssp->ssp_count].quirks = 0; + + if (quirks) { + buf = strdup(quirks); + if (!buf) + return -ENOMEM; + + token = strtok(buf, delim); + + while (token) { + if (!strcmp(token, "lbm_mode")) + ssp->ssp_prm[ssp->ssp_count].quirks |= SSP_INTEL_QUIRK_LBM; + else if (!strcmp(token, "bt_sideband")) + ssp->ssp_prm[ssp->ssp_count].quirks |= SSP_INTEL_QUIRK_BT_SIDEBAND; + else if (!strcmp(token, "render_feedback")) { + if (!strcmp(dir, "duplex")) + ssp->ssp_prm[ssp->ssp_count].quirks |= SSP_INTEL_QUIRK_RENDER_FEEDBACK; + } else { + fprintf(stderr, "ssp_set_params(): unknown quirk %s\n", token); + free(buf); + return -EINVAL; + } + + token = strtok(NULL, delim); + } + + free(buf); + } + + /* reset hw config count for this ssp instance */ + ssp->ssp_hw_config_count[ssp->ssp_count] = 0; + + return 0; +} + +int ssp_hw_set_params(struct intel_nhlt_params *nhlt, const char *format, + const char *mclk ATTRIBUTE_UNUSED, + const char *bclk, const char *bclk_invert, const char *fsync, + const char *fsync_invert, int mclk_freq, int bclk_freq, int fsync_freq, + int tdm_slots, int tdm_slot_width, int tx_slots, int rx_slots) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + uint32_t hwi; + + if (!ssp) + return -EINVAL; + + /* check that the strings are defined ?*/ + + /* compose format out of clock related string variables */ + hwi = ssp->ssp_hw_config_count[ssp->ssp_count]; + + if (!strcmp(format, "I2S")) { + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format = SSP_FMT_I2S; + } else if (!strcmp(format, "RIGHT_J")) { + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format = SSP_FMT_RIGHT_J; + } else if (!strcmp(format, "LEFT_J")) { + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format = SSP_FMT_LEFT_J; + } else if (!strcmp(format, "DSP_A")) { + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format = SSP_FMT_DSP_A; + } else if (!strcmp(format, "DSP_B")) { + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format = SSP_FMT_DSP_B; + } else { + fprintf(stderr, "no valid format specified for ssp: %s\n", format); + return -EINVAL; + } + + /* clock directions wrt codec */ + if (bclk && !strcmp(bclk, "codec_provider")) { + /* codec is bclk provider */ + if (fsync && !strcmp(fsync, "codec_provider")) + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_CBP_CFP; + else + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_CBP_CFC; + } else { + /* codec is bclk consumer */ + if (fsync && !strcmp(fsync, "codec_provider")) + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_CBC_CFP; + else + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_CBC_CFC; + } + + /* inverted clocks ? */ + if (bclk_invert && !strcmp(bclk_invert, "true")) { + if (fsync_invert && !strcmp(fsync_invert, "true")) + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_IB_IF; + else + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_IB_NF; + } else { + if (fsync_invert && !strcmp(fsync_invert, "true")) + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_NB_IF; + else + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].format |= SSP_FMT_NB_NF; + } + + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].mclk_rate = mclk_freq; + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].bclk_rate = bclk_freq; + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].fsync_rate = fsync_freq; + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].tdm_slots = tdm_slots; + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].tdm_slot_width = tdm_slot_width; + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].tx_slots = tx_slots; + ssp->ssp_prm[ssp->ssp_count].hw_cfg[hwi].rx_slots = rx_slots; + + ssp->ssp_hw_config_count[ssp->ssp_count]++; + + return 0; +} + +int ssp_mn_set_params(struct intel_nhlt_params *nhlt, int m_div, int n_div) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + + if (di < 0 || hwi < 0) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_MN_DIVIDER_CONTROLS); + + ssp->ssp_prm[di].aux_cfg[hwi].mn.m_div = m_div; + ssp->ssp_prm[di].aux_cfg[hwi].mn.n_div = n_div; + + return 0; +} + +int ssp_clk_set_params(struct intel_nhlt_params *nhlt, int clock_warm_up, int mclk, int warm_up_ovr, + int clock_stop_delay, int keep_running, int clock_stop_ovr) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + + if (di < 0 || hwi < 0) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_DMA_CLK_CONTROLS); + + ssp->ssp_prm[di].aux_cfg[hwi].clk.clock_warm_up = clock_warm_up; + ssp->ssp_prm[di].aux_cfg[hwi].clk.mclk = mclk; + ssp->ssp_prm[di].aux_cfg[hwi].clk.warm_up_ovr = warm_up_ovr; + ssp->ssp_prm[di].aux_cfg[hwi].clk.clock_stop_delay = clock_stop_delay; + ssp->ssp_prm[di].aux_cfg[hwi].clk.keep_running = keep_running; + ssp->ssp_prm[di].aux_cfg[hwi].clk.clock_stop_ovr = clock_stop_ovr; + + return 0; +} + +int ssp_tr_start_set_params(struct intel_nhlt_params *nhlt, int sampling_frequency, + int bit_depth, int channel_map, int channel_config, + int interleaving_style, int number_of_channels, + int valid_bit_depth, int sample_type) +{ + struct intel_ssp_params *ssp; + struct ssp_aux_config_tr *tr; + int di, hwi; + + ssp = (struct intel_ssp_params *)nhlt->ssp_params; + di = ssp->ssp_count; + hwi = ssp->ssp_hw_config_count[di]; + if (di < 0 || hwi < 0) + return -EINVAL; + tr = (struct ssp_aux_config_tr *)&(ssp->ssp_prm[di].aux_cfg[hwi].tr_start); + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_DMA_TRANSMISSION_START); + + tr->sampling_frequency = sampling_frequency; + tr->bit_depth = bit_depth; + tr->channel_map = channel_map; + tr->channel_config = channel_config; + tr->interleaving_style = interleaving_style; + tr->number_of_channels = number_of_channels; + tr->valid_bit_depth = valid_bit_depth; + tr->sample_type = sample_type; + + return 0; +} + +int ssp_tr_stop_set_params(struct intel_nhlt_params *nhlt, int sampling_frequency, + int bit_depth, int channel_map, int channel_config, + int interleaving_style, int number_of_channels, + int valid_bit_depth, int sample_type) +{ + struct intel_ssp_params *ssp; + struct ssp_aux_config_tr *tr; + int di, hwi; + + ssp = (struct intel_ssp_params *)nhlt->ssp_params; + di = ssp->ssp_count; + hwi = ssp->ssp_hw_config_count[di]; + if (di < 0 || hwi < 0) + return -EINVAL; + tr = (struct ssp_aux_config_tr *)&(ssp->ssp_prm[di].aux_cfg[hwi].tr_stop); + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_DMA_TRANSMISSION_STOP); + + tr->sampling_frequency = sampling_frequency; + tr->bit_depth = bit_depth; + tr->channel_map = channel_map; + tr->channel_config = channel_config; + tr->interleaving_style = interleaving_style; + tr->number_of_channels = number_of_channels; + tr->valid_bit_depth = valid_bit_depth; + tr->sample_type = sample_type; + + return 0; +} + +int ssp_run_set_params(struct intel_nhlt_params *nhlt, int always_run) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + + if (di < 0 || hwi < 0) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_DMA_ALWAYS_RUNNING_MODE); + + ssp->ssp_prm[di].aux_cfg[hwi].run.always_run = always_run; + + return 0; +} + +int ssp_sync_set_params(struct intel_nhlt_params *nhlt, int sync_denominator) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + + if (di < 0 || hwi < 0) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_DMA_SYNC_DATA); + + ssp->ssp_prm[di].aux_cfg[hwi].sync.sync_denominator = sync_denominator; + + return 0; +} + +int ssp_node_set_params(struct intel_nhlt_params *nhlt, int node_id, int sampling_rate) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + int count; + + if (di < 0 || hwi < 0) + return -EINVAL; + + count = ssp->ssp_prm[di].aux_cfg[hwi].sync.count; + if (count > SSP_MAX_DAIS) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].sync.nodes[count].node_id = node_id; + ssp->ssp_prm[di].aux_cfg[hwi].sync.nodes[count].sampling_rate = sampling_rate; + + ssp->ssp_prm[di].aux_cfg[hwi].sync.count++; + + return 0; +} + +int ssp_ext_set_params(struct intel_nhlt_params *nhlt, int mclk_policy_override, + int mclk_always_running, int mclk_starts_on_gtw_init, int mclk_starts_on_run, + int mclk_starts_on_pause, int mclk_stops_on_pause, int mclk_stops_on_reset, + int bclk_policy_override, int bclk_always_running, + int bclk_starts_on_gtw_init, int bclk_starts_on_run, + int bclk_starts_on_pause, int bclk_stops_on_pause, int bclk_stops_on_reset, + int sync_policy_override, int sync_always_running, + int sync_starts_on_gtw_init, int sync_starts_on_run, + int sync_starts_on_pause, int sync_stops_on_pause, int sync_stops_on_reset) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + + if (di < 0 || hwi < 0) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_DMA_CLK_CONTROLS_EXT); + + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_policy_override = mclk_policy_override; + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_always_running = mclk_always_running; + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_starts_on_gtw_init = mclk_starts_on_gtw_init; + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_starts_on_run = mclk_starts_on_run; + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_starts_on_pause = mclk_starts_on_pause; + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_stops_on_pause = mclk_stops_on_pause; + ssp->ssp_prm[di].aux_cfg[hwi].ext.mclk_stops_on_reset = mclk_stops_on_reset; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_policy_override = bclk_policy_override; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_always_running = bclk_always_running; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_starts_on_gtw_init = bclk_starts_on_gtw_init; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_starts_on_run = bclk_starts_on_run; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_starts_on_pause = bclk_starts_on_pause; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_stops_on_pause = bclk_stops_on_pause; + ssp->ssp_prm[di].aux_cfg[hwi].ext.bclk_stops_on_reset = bclk_stops_on_reset; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_policy_override = sync_policy_override; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_always_running = sync_always_running; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_starts_on_gtw_init = sync_starts_on_gtw_init; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_starts_on_run = sync_starts_on_run; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_starts_on_pause = sync_starts_on_pause; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_stops_on_pause = sync_stops_on_pause; + ssp->ssp_prm[di].aux_cfg[hwi].ext.sync_stops_on_reset = sync_stops_on_reset; + + return 0; +} + +int ssp_link_set_params(struct intel_nhlt_params *nhlt, int clock_source) +{ + struct intel_ssp_params *ssp = (struct intel_ssp_params *)nhlt->ssp_params; + int di = ssp->ssp_count; + int hwi = ssp->ssp_hw_config_count[di]; + + if (di < 0 || hwi < 0) + return -EINVAL; + + ssp->ssp_prm[di].aux_cfg[hwi].enabled |= BIT(SSP_LINK_CLK_SOURCE); + + ssp->ssp_prm[di].aux_cfg[hwi].link.clock_source = clock_source; + + return 0; +} + +/* init ssp parameters, should be called before parsing dais */ +int ssp_init_params(struct intel_nhlt_params *nhlt) +{ + struct intel_ssp_params *ssp; + int i, j; + + ssp = calloc(1, sizeof(struct intel_ssp_params)); + if (!ssp) + return -EINVAL; + + nhlt->ssp_params = ssp; + ssp->ssp_count = 0; + + for (i = 0; i < SSP_MAX_DAIS; i++) { + ssp->ssp_hw_config_count[i] = 0; + for (j = 0; j < SSP_MAX_HW_CONFIG; j++) + ssp->ssp_prm[i].aux_cfg[j].sync.count = 0; + } + + return 0; +} diff --git a/topology/nhlt/intel/ssp/ssp-process.h b/topology/nhlt/intel/ssp/ssp-process.h new file mode 100644 index 0000000..ea80482 --- /dev/null +++ b/topology/nhlt/intel/ssp/ssp-process.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Liam Girdwood +// Keyon Jie +// Rander Wang +// Jaska Uimonen + +#ifndef __SSP_PROCESS_H +#define __SSP_PROCESS_H + +#include + +/* initialize and set default values before parsing */ +int ssp_init_params(struct intel_nhlt_params *nhlt); + +/* set parameters when parsing topology2 conf */ +int ssp_set_params(struct intel_nhlt_params *nhlt, const char *dir, int dai_index, int io_clk, + int bclk_delay, int sample_bits, int mclk_id, int clks_control, + int frame_pulse_width, const char *tdm_padding_per_slot, const char *quirks, + int version); +int ssp_hw_set_params(struct intel_nhlt_params *nhlt, const char *format, const char *mclk, + const char *bclk, const char *bclk_invert, const char *fsync, + const char *fsync_invert, int mclk_freq, int bclk_freq, int fsync_freq, + int tdm_slots, int tdm_slot_width, int tx_slots, int rx_slots); + +/* set aux params when parsing topology2 conf */ +int ssp_mn_set_params(struct intel_nhlt_params *nhlt, int m_div, int n_div); +int ssp_clk_set_params(struct intel_nhlt_params *nhlt, int clock_warm_up, int mclk, int warm_up_ovr, + int clock_stop_delay, int keep_running, int clock_stop_ovr); +int ssp_tr_start_set_params(struct intel_nhlt_params *nhlt, int sampling_frequency, + int bit_depth, int channel_map, int hannel_config, + int interleaving_style, int number_of_channels, + int valid_bit_depth, int sample_type); +int ssp_tr_stop_set_params(struct intel_nhlt_params *nhlt, int sampling_frequency, + int bit_depth, int channel_map, int hannel_config, + int interleaving_style, int number_of_channels, + int valid_bit_depth, int sample_type); +int ssp_run_set_params(struct intel_nhlt_params *nhlt, int always_run); +int ssp_sync_set_params(struct intel_nhlt_params *nhlt, int sync_denominator); +int ssp_node_set_params(struct intel_nhlt_params *nhlt, int node_id, int sampling_rate); +int ssp_ext_set_params(struct intel_nhlt_params *nhlt, int mclk_policy_override, + int mclk_always_running, int mclk_starts_on_gtw_init, int mclk_starts_on_run, + int mclk_starts_on_pause, int mclk_stops_on_pause, int mclk_stops_on_reset, + int bclk_policy_override, int bclk_always_running, + int bclk_starts_on_gtw_init, int bclk_starts_on_run, + int bclk_starts_on_pause, int bclk_stops_on_pause, int bclk_stops_on_reset, + int sync_policy_override, int sync_always_running, + int sync_starts_on_gtw_init, int sync_starts_on_run, + int sync_starts_on_pause, int sync_stops_on_pause, int sync_stops_on_reset); +int ssp_link_set_params(struct intel_nhlt_params *nhlt, int clock_source); + +/* calculate the blob after parsing the values*/ +int ssp_calculate(struct intel_nhlt_params *nhlt); +/* get spec parameters when building the nhlt endpoint */ +int ssp_get_params(struct intel_nhlt_params *nhlt, int dai_index, uint32_t *virtualbus_id, + uint32_t *formats_count, uint32_t *device_type, uint32_t *direction); +int ssp_get_hw_params(struct intel_nhlt_params *nhlt, int dai_index, int hw_index, + uint32_t *sample_rate, uint16_t *channel_count, uint32_t *bits_per_sample); +int ssp_get_dir(struct intel_nhlt_params *nhlt, int dai_index, uint8_t *dir); +/* get vendor specific blob when building the nhlt endpoint */ +int ssp_get_vendor_blob_count(struct intel_nhlt_params *nhlt); +int ssp_get_vendor_blob_size(struct intel_nhlt_params *nhlt, int dai_index, int hw_config_index, + size_t *size); +int ssp_get_vendor_blob(struct intel_nhlt_params *nhlt, uint8_t *vendor_blob, int dai_index, + int hw_config_index); + +#endif diff --git a/topology/nhlt/nhlt-processor.c b/topology/nhlt/nhlt-processor.c new file mode 100644 index 0000000..8197201 --- /dev/null +++ b/topology/nhlt/nhlt-processor.c @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include "pre-process-external.h" +#include "nhlt.h" +#include "intel/intel-nhlt.h" +#include "intel/dmic-nhlt.h" +#include "intel/ssp-nhlt.h" + +#define MAX_ENDPOINT_COUNT 20 +#define ALSA_BYTE_CHARS 5 +#define SOF_ABI_CHARS 29 +#define SOF_MANIFEST_DATA_TYPE_NHLT 1 + +struct sof_manifest_tlv { + uint32_t type; + uint32_t size; + uint8_t data[]; +} __attribute__((packed)); + +struct sof_manifest { + uint16_t abi_major; + uint16_t abi_minor; + uint16_t abi_patch; + uint16_t count; + struct sof_manifest_tlv items[]; +} __attribute__((packed)); + +#ifdef NHLT_DEBUG +static void debug_print_nhlt(struct nhlt *blob, struct endpoint_descriptor **eps) +{ + uint8_t *top_p = (uint8_t *)blob; + struct endpoint_descriptor *ep; + uint8_t *ep_p; + int i, j, k, lines, remain; + + fprintf(stdout, "printing nhlt as bytes:\n"); + + lines = sizeof(struct nhlt) / 8; + remain = sizeof(struct nhlt) % 8; + for (i = 0; i < lines; i++) { + for (j = 0; j < 8; j++) { + fprintf(stdout, "0x%02x,", *top_p); + top_p++; + } + fprintf(stdout, "\n"); + } + for (i = 0; i < remain; i++) { + fprintf(stdout, "0x%02x,", *top_p); + top_p++; + } + fprintf(stdout, "\n\n"); + + for (i = 0; i < blob->endpoint_count; i++) { + ep = eps[i]; + ep_p = (uint8_t *)ep; + lines = ep->length / 8; + remain = ep->length % 8; + for (j = 0; j < lines; j++) { + for (k = 0; k < 8; k++) { + fprintf(stdout, "0x%02x,", *ep_p); + ep_p++; + } + fprintf(stdout, "\n"); + } + for (j = 0; j < remain; j++) { + fprintf(stdout, "0x%02x,", *ep_p); + ep_p++; + } + fprintf(stdout, "\n"); + } + + fprintf(stdout, "\n"); +} +#else +static void debug_print_nhlt(struct nhlt *blob ATTRIBUTE_UNUSED, + struct endpoint_descriptor **eps ATTRIBUTE_UNUSED) {} +#endif + +static int print_as_hex_bytes(uint8_t *manifest_buffer, uint32_t manifest_size, + uint8_t *nhlt_buffer, uint32_t nhlt_size, char **src) +{ + char *bytes_string_buffer; + char *dst; + unsigned int i; + + bytes_string_buffer = calloc((manifest_size + nhlt_size) * ALSA_BYTE_CHARS + 1, + sizeof(uint8_t)); + if (!bytes_string_buffer) + return -ENOMEM; + + dst = bytes_string_buffer; + for (i = 0; i < manifest_size; i++) { + snprintf(dst, ALSA_BYTE_CHARS + 1, "0x%02x,", *manifest_buffer); + dst += ALSA_BYTE_CHARS; + manifest_buffer++; + } + + for (i = 0; i < nhlt_size; i++) { + snprintf(dst, ALSA_BYTE_CHARS + 1, "0x%02x,", *nhlt_buffer); + dst += ALSA_BYTE_CHARS; + nhlt_buffer++; + } + + /* remove the last comma... */ + dst--; + *dst = '\0'; + + *src = bytes_string_buffer; + + return 0; +} + +static int merge_manifest_data(snd_config_t *cfg, uint8_t *manifest_buffer, uint32_t manifest_size, + uint8_t *nhlt_buffer, uint32_t nhlt_size) +{ + const char *data_name = "SOF ABI"; + snd_config_t *data_section; + snd_config_t *manifest; + snd_config_t *old_bytes; + snd_config_t *new_bytes; + char *src = NULL; + int ret; + + /* merge manifest struct and nhlt bytes as new config into existing SectionData*/ + ret = snd_config_search(cfg, "SectionData", &data_section); + if (ret < 0) + return ret; + + ret = snd_config_search(data_section, data_name, &manifest); + if (ret < 0) + return ret; + + ret = snd_config_search(manifest, "bytes", &old_bytes); + if (ret < 0) + return ret; + + ret = snd_config_make(&new_bytes, "bytes", SND_CONFIG_TYPE_STRING); + if (ret < 0) + goto err; + + ret = print_as_hex_bytes(manifest_buffer, manifest_size, nhlt_buffer, nhlt_size, &src); + if (ret < 0) + goto err; + + ret = snd_config_set_string(new_bytes, src); + if (ret < 0) + goto err; + + ret = snd_config_merge(old_bytes, new_bytes, true); + if (ret < 0) + goto err; + + free(src); + + return 0; +err: + if (new_bytes) + snd_config_delete(new_bytes); + if (src) + free(src); + + return ret; +} + +static void save_nhlt_binary(struct nhlt *blob, struct endpoint_descriptor **eps, snd_config_t *cfg) +{ + const char *bin_file = NULL; + snd_config_t *defines; + FILE *fp; + int ret; + int i; + + ret = snd_config_search(cfg, "Define.NHLT_BIN", &defines); + if (ret < 0) + return; + + if (snd_config_get_string(defines, &bin_file) < 0) + return; + + fp = fopen(bin_file, "wb"); + if (fp == NULL) { + fprintf(stderr, "can't open nhlt binary output file %s\n", bin_file); + return; + } + + fprintf(stdout, "saving nhlt as binary in %s\n", bin_file); + + fwrite(blob, 1, sizeof(struct nhlt), fp); + + for (i = 0; i < blob->endpoint_count; i++) + fwrite(eps[i], eps[i]->length, sizeof(uint8_t), fp); + + fclose(fp); +} + +static int manifest_create(snd_config_t *input, uint8_t **manifest_buffer, uint32_t *size, uint32_t nhlt_size) +{ + struct sof_manifest_tlv manifest_tlv; + struct sof_manifest manifest; + snd_config_t *data_section; + snd_config_t *data; + snd_config_t *old_bytes; + uint32_t manifest_size; + uint8_t *byte_buffer; + const char *abi; + uint8_t *top_p; + uint8_t *dst; + int ret; + unsigned int i; + + ret = snd_config_search(input, "SectionData", &data_section); + if (ret < 0) + return ret; + + ret = snd_config_search(data_section, "SOF ABI", &data); + if (ret < 0) + return ret; + + ret = snd_config_search(data, "bytes", &old_bytes); + if (ret < 0) + return ret; + + ret = snd_config_get_string(old_bytes, &abi); + if (ret < 0) + return ret; + + /* we have something funny in abi string */ + if (strlen(abi) != SOF_ABI_CHARS) + return -EINVAL; + + manifest.count = 1; + manifest_tlv.type = SOF_MANIFEST_DATA_TYPE_NHLT; + manifest_tlv.size = nhlt_size; + + manifest_size = sizeof(struct sof_manifest) + sizeof(struct sof_manifest_tlv); + byte_buffer = calloc(manifest_size, sizeof(uint8_t)); + if (!byte_buffer) + return -ENOMEM; + + *size = manifest_size; + + dst = byte_buffer; + + /* copy the ABI version bytes */ + for (i = 0; i < 6; i++) + sscanf(&abi[i * ALSA_BYTE_CHARS], "%" SCNx8, dst++); + + /* set the count */ + *dst++ = manifest.count; + *dst++ = manifest.count >> 2; + + top_p = (uint8_t *)&manifest_tlv; + for (i = 0; i < sizeof(struct sof_manifest_tlv); i++) + *dst++ = *top_p++; + + *manifest_buffer = byte_buffer; + + return 0; +} + +static int nhlt_get_flat_buffer(struct nhlt *blob, struct endpoint_descriptor **eps, + uint32_t eps_count, uint32_t *size, uint8_t **nhlt_buffer) +{ + uint8_t *top_p = (uint8_t *)blob; + struct endpoint_descriptor *ep; + uint8_t *byte_buffer; + uint32_t nhlt_size; + uint8_t *ep_p; + uint8_t *dst; + unsigned int i, j; + + /* get blob total size */ + nhlt_size = sizeof(struct nhlt); + for (i = 0; i < eps_count; i++) { + if (eps[i]) + nhlt_size += eps[i]->length; + } + + *size = nhlt_size; + + byte_buffer = calloc(nhlt_size, sizeof(uint8_t)); + if (!byte_buffer) + return -ENOMEM; + + dst = byte_buffer; + for (i = 0; i < sizeof(struct nhlt); i++) + *dst++ = *top_p++; + + for (i = 0; i < blob->endpoint_count; i++) { + ep = eps[i]; + ep_p = (uint8_t *)ep; + for (j = 0; j < ep->length; j++) + *dst++ = *ep_p++; + } + + *nhlt_buffer = byte_buffer; + + return 0; +} + +/* called at the end of topology pre-processing, create flat buffer from variable size nhlt */ +static int nhlt_create(struct intel_nhlt_params *nhlt, snd_config_t *input, + snd_config_t *output ATTRIBUTE_UNUSED, + uint8_t **nhlt_buffer, uint32_t *nhlt_size) +{ + struct endpoint_descriptor *eps[MAX_ENDPOINT_COUNT]; + int eps_count = 0; + struct nhlt blob; + uint32_t size; + uint8_t dir; + int ret; + int i; + + for (i = 0; i < MAX_ENDPOINT_COUNT; i++) + eps[i] = NULL; + + /* we always have only 0 or 1 dmic ep */ + for (i = 0; i < nhlt_dmic_get_ep_count(nhlt); i++) { + ret = nhlt_dmic_get_ep(nhlt, &eps[eps_count], i); + if (ret < 0) + return -EINVAL; + eps_count++; + } + + /* we can have 0 to several ssp eps */ + for (i = 0; i < nhlt_ssp_get_ep_count(nhlt); i++) { + nhlt_ssp_get_dir(nhlt, i, &dir); + /* duplicate endpoint for duplex dai */ + if (dir > NHLT_ENDPOINT_DIRECTION_FEEDBACK_FOR_RENDER) { + ret = nhlt_ssp_get_ep(nhlt, &eps[eps_count], i, + NHLT_ENDPOINT_DIRECTION_RENDER); + if (ret < 0) + goto err; + eps_count++; + ret = nhlt_ssp_get_ep(nhlt, &eps[eps_count], i, + NHLT_ENDPOINT_DIRECTION_CAPTURE); + } else { + ret = nhlt_ssp_get_ep(nhlt, &eps[eps_count], i, dir); + } + if (ret < 0) + goto err; + eps_count++; + } + + /* we don't have endpoints */ + if (!eps_count) + return 0; + + uint8_t sig[4] = {'N', 'H', 'L', 'T'}; + blob.efi_acpi.signature = *((uint32_t *)sig); + blob.efi_acpi.length = 0; + blob.efi_acpi.revision = 0; + blob.efi_acpi.checksum = 0; + for (i = 0; i < 6; i++) + blob.efi_acpi.oem_id[i] = 0; + blob.efi_acpi.oem_table_id = 0; + blob.efi_acpi.oem_revision = 0; + blob.efi_acpi.creator_id = 0; + blob.efi_acpi.creator_revision = 0; + + blob.endpoint_count = eps_count; + + /* get blob total size */ + size = sizeof(struct nhlt); + for (i = 0; i < eps_count; i++) { + if (eps[i]) + size += eps[i]->length; + } + + /* add the total length to top level struct */ + blob.efi_acpi.length = size; + + debug_print_nhlt(&blob, eps); + + save_nhlt_binary(&blob, eps, input); + + ret = nhlt_get_flat_buffer(&blob, eps, eps_count, nhlt_size, nhlt_buffer); + +err: + /* remove all enpoints */ + for (i = 0; i < eps_count; i++) + free(eps[i]); + + return ret; +} + +static int do_nhlt(struct intel_nhlt_params *nhlt, snd_config_t *input, snd_config_t *output) +{ + uint8_t *manifest_buffer = NULL; + uint8_t *nhlt_buffer = NULL; + uint32_t manifest_size; + uint32_t nhlt_size = 0; + int ret = 0; + + ret = nhlt_create(nhlt, input, output, &nhlt_buffer, &nhlt_size); + if (ret) { + fprintf(stderr, "can't create nhlt blob, err %d\n", ret); + return ret; + } + + ret = manifest_create(output, &manifest_buffer, &manifest_size, nhlt_size); + if (ret) { + fprintf(stderr, "can't re-create manifest, err %d\n", ret); + goto err; + } + + ret = merge_manifest_data(output, manifest_buffer, manifest_size, nhlt_buffer, nhlt_size); + if (ret) + fprintf(stderr, "can't merge manifest data, err %d\n", ret); + +err: + if (manifest_buffer) + free(manifest_buffer); + if (nhlt_buffer) + free(nhlt_buffer); + + return ret; +} + +SND_TOPOLOGY_PLUGIN_DEFINE_FUNC(nhlt) +{ + snd_config_iterator_t i, i2, next, next2; + struct intel_nhlt_params nhlt; + snd_config_t *n, *n2; + snd_config_t *items; + const char *id, *id2; + int ret; + + /* initialize the internal structs */ + ret = nhlt_ssp_init_params(&nhlt); + if (ret < 0) + return ret; + + ret = nhlt_dmic_init_params(&nhlt); + if (ret < 0) + return ret; + + /* find DAIs and set internal parameters */ + ret = snd_config_search(input, "Object.Dai", &items); + if (ret < 0) + return ret; + + snd_config_for_each(i, next, items) { + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + snd_config_for_each(i2, next2, n) { + n2 = snd_config_iterator_entry(i2); + + if (snd_config_get_id(n2, &id2) < 0) + continue; + + /* set dai parameters here */ + if (!strncmp(id, "DMIC", 4)) { + ret = nhlt_dmic_set_params(&nhlt, n2, input); + if (ret < 0) + return ret; + } + + if (!strncmp(id, "SSP", 3)) { + ret = nhlt_ssp_set_params(&nhlt, n2, input); + if (ret < 0) + return ret; + } + } + } + + /* create the nhlt blob from internal structs */ + ret = do_nhlt(&nhlt, input, output); + if (ret) + fprintf(stderr, "error in nhlt processing\n"); + + free(nhlt.ssp_params); + free(nhlt.dmic_params); + + return 0; +} diff --git a/topology/nhlt/nhlt.h b/topology/nhlt/nhlt.h new file mode 100644 index 0000000..c7d573a --- /dev/null +++ b/topology/nhlt/nhlt.h @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2021 Intel Corporation. All rights reserved. +// +// Author: Jaska Uimonen + +#ifndef __NHLT_H +#define __NHLT_H + +#include + +/* + * Nhlt defines and structs are derived from: + * https://01.org/sites/default/files/595976_intel_sst_nhlt.pdf + * + * Acpi description header for example: + * https://uefi.org/sites/default/files/resources/ACPI_6_3_final_Jan30.pdf + * + * Idea is to generate similar blob as you would get from: + * 'cat /sys/firmware/acpi/tables/NHLT' + * + */ +#define NHLT_LINK_TYPE_HDAUDIO 0 +#define NHLT_LINK_TYPE_DSP 1 +#define NHLT_LINK_TYPE_PDM 2 +#define NHLT_LINK_TYPE_SSP 3 +#define NHLT_LINK_TYPE_SLIMBUS 4 +#define NHLT_LINK_TYPE_SOUNDWIRE 5 + +#define NHLT_VENDOR_ID_INTEL 0x8086 + +#define NHLT_DEVICE_ID_INTEL_PDM_DMIC 0xAE20 +#define NHLT_DEVICE_ID_INTEL_BT_SIDEBAND 0xAE30 +#define NHLT_DEVICE_ID_INTEL_I2S_TDM 0xAE34 + +#define NHLT_DEVICE_TYPE_SSP_BT_SIDEBAND 0 +#define NHLT_DEVICE_TYPE_SSP_FM 1 +#define NHLT_DEVICE_TYPE_SSP_MODEM 2 +#define NHLT_DEVICE_TYPE_SSP_ANALOG 4 + +#define NHLT_ENDPOINT_DIRECTION_RENDER 0 +#define NHLT_ENDPOINT_DIRECTION_CAPTURE 1 +#define NHLT_ENDPOINT_DIRECTION_RENDER_WITH_LOOPBACK 2 +#define NHLT_ENDPOINT_DIRECTION_FEEDBACK_FOR_RENDER 3 + +#define NHLT_DEVICE_CONFIG_TYPE_GENERIC 0 +#define NHLT_DEVICE_CONFIG_TYPE_MICARRAY 1 +#define NHLT_DEVICE_CONFIG_TYPE_RENDERWITHLOOPBACK 2 +#define NHLT_DEVICE_CONFIG_TYPE_RENDERFEEDBACK 3 + +#define NHLT_MIC_ARRAY_TYPE_LINEAR_2_ELEMENT_SMALL 0xA +#define NHLT_MIC_ARRAY_TYPE_LINEAR_2_ELEMENT_BIG 0xB +#define NHLT_MIC_ARRAY_TYPE_LINEAR_4_ELEMENT_1ST_GEOMETRY 0xC +#define NHLT_MIC_ARRAY_TYPE_PLANAR_4_ELEMENT_L_SHAPED 0xD +#define NHLT_MIC_ARRAY_TYPE_PLANAR_4_ELEMENT_2ND_GEOMETRY 0xE +#define NHLT_MIC_ARRAY_TYPE_VENDOR_DEFINED 0xF + +#define NHLT_MIC_ARRAY_NO_EXTENSION 0x0 +#define NHLT_MIC_ARRAY_SNR_AND_SENSITIVITY_EXTENSION 0x1 + +#define NHLT_MIC_TYPE_OMNIDIRECTIONAL 0 +#define NHLT_MIC_TYPE_SUBCARDIOID 1 +#define NHLT_MIC_TYPE_CARDIOID 2 +#define NHLT_MIC_TYPE_SUPERCARDIOID 3 +#define NHLT_MIC_TYPE_HYPERCARDIOID 4 +#define NHLT_MIC_TYPE_8SHAPED 5 +#define NHLT_MIC_TYPE_RESERVED 6 +#define NHLT_MIC_TYPE_VENDORDEFINED 7 + +#define NHLT_MIC_POSITION_TOP 0 +#define NHLT_MIC_POSITION_BOTTOM 1 +#define NHLT_MIC_POSITION_LEFT 2 +#define NHLT_MIC_POSITION_RIGHT 3 +#define NHLT_MIC_POSITION_FRONT 4 /*(default) */ +#define NHLT_MIC_POSITION_REAR 5 + +struct specific_config { + uint32_t capabilities_size; /* does not include size of this field */ + uint8_t capabilities[]; +} __attribute__((packed)); + +struct device_specific_config { + uint8_t virtual_slot; + uint8_t config_type; +} __attribute__((packed)); + +struct ssp_device_specific_config { + struct specific_config config; + struct device_specific_config device_config; +} __attribute__((packed)); + +struct mic_snr_sensitivity_extension { + uint32_t snr; + uint32_t sensitivity; +} __attribute__((packed)); + +struct mic_vendor_config { + uint8_t type; + uint8_t panel; + uint32_t speaker_position_distance; + uint32_t horizontal_offset; + uint32_t vertical_offset; + uint8_t frequency_low_band; + uint8_t frequency_high_band; + uint16_t direction_angle; + uint16_t elevation_angle; + uint16_t vertical_angle_begin; + uint16_t vertical_angle_end; + uint16_t horizontal_angle_begin; + uint16_t horizontal_angle_end; +} __attribute__((packed)); + +struct mic_array_device_specific_config { + struct specific_config config; + struct device_specific_config device_config; + uint8_t array_type_ex; +} __attribute__((packed)); + +struct mic_array_device_specific_vendor_config { + struct specific_config config; + struct device_specific_config device_config; + uint8_t array_type_ex; + uint8_t number_of_microphones; + uint8_t mic_vendor_configs[]; +} __attribute__((packed)); + +struct WAVEFORMATEXTENSIBLE { + uint16_t wFormatTag; + uint16_t nChannels; + uint32_t nSamplesPerSec; + uint32_t nAvgBytesPerSec; + uint16_t nBlockAlign; + uint16_t wBitsPerSample; + uint16_t cbSize; + uint16_t wValidBitsPerSample; + uint32_t dwChannelMask; + uint32_t SubFormat[4]; +} __attribute__((packed)); + +struct format_config { + struct WAVEFORMATEXTENSIBLE format; + struct specific_config vendor_blob; +} __attribute__((packed)); + +struct formats_config { + uint8_t formats_count; + uint8_t f_configs[]; +} __attribute__((packed)); + +struct endpoint_descriptor { + uint32_t length; /* includes the length of this field also */ + uint8_t link_type; + uint8_t instance_id; + uint16_t vendor_id; + uint16_t device_id; + uint16_t revision_id; + uint32_t subsystem_id; + uint8_t device_type; + uint8_t direction; + uint8_t virtualbus_id; +} __attribute__((packed)); + +struct efi_acpi_description_header { + uint32_t signature; + uint32_t length; + uint8_t revision; + uint8_t checksum; + uint8_t oem_id[6]; + uint64_t oem_table_id; + uint32_t oem_revision; + uint32_t creator_id; + uint32_t creator_revision; +} __attribute__((packed)); + +struct nhlt { + struct efi_acpi_description_header efi_acpi; + uint8_t endpoint_count; + uint8_t endpoints[]; +} __attribute__((packed)); + +#endif /* __NHLT_H */ diff --git a/topology/pre-process-class.c b/topology/pre-process-class.c new file mode 100644 index 0000000..bedcebe --- /dev/null +++ b/topology/pre-process-class.c @@ -0,0 +1,301 @@ +/* + Copyright(c) 2021 Intel Corporation + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + The full GNU General Public License is included in this distribution + in the file called LICENSE.GPL. +*/ +#include "aconfig.h" +#include +#include +#include +#include +#include +#include "topology.h" +#include "pre-processor.h" + +bool tplg_class_is_attribute_check(const char *attr, snd_config_t *class_cfg, char *category) +{ + snd_config_iterator_t i, next; + snd_config_t *cfg, *n; + int ret; + + ret = snd_config_search(class_cfg, category, &cfg); + if (ret < 0) + return false; + + snd_config_for_each(i, next, cfg) { + const char *id, *s; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (snd_config_get_string(n, &s) < 0) + continue; + + if (!strcmp(attr, s)) + return true; + } + + return false; +} + +/* check if attribute is mandatory */ +bool tplg_class_is_attribute_mandatory(const char *attr, snd_config_t *class_cfg) +{ + return tplg_class_is_attribute_check(attr, class_cfg, "attributes.mandatory"); +} + +/* check if attribute is immutable */ +bool tplg_class_is_attribute_immutable(const char *attr, snd_config_t *class_cfg) +{ + return tplg_class_is_attribute_check(attr, class_cfg, "attributes.immutable"); +} + +/* check if attribute is unique */ +bool tplg_class_is_attribute_unique(const char *attr, snd_config_t *class_cfg) +{ + snd_config_t *unique; + const char *s; + int ret; + + ret = snd_config_search(class_cfg, "attributes.unique", &unique); + if (ret < 0) + return false; + + if (snd_config_get_string(unique, &s) < 0) + return false; + + if (!strcmp(attr, s)) + return true; + + return false; +} + +/* + * Helper function to look up class definition from the Object config. + * ex: For an object declaration, Object.Widget.pga.0{}, return the config correspdonding to + * Class.Widget.pga{}. Note that input config , "cfg" does not include the "Object" node. + */ +snd_config_t *tplg_class_lookup(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg) +{ + snd_config_iterator_t first, end; + snd_config_t *class, *class_cfg = NULL; + const char *class_type, *class_name; + char *class_config_id; + int ret; + + if (snd_config_get_id(cfg, &class_type) < 0) + return NULL; + + first = snd_config_iterator_first(cfg); + end = snd_config_iterator_end(cfg); + + if (first == end) { + SNDERR("No class name provided for object type: %s\n", class_type); + return NULL; + } + + class = snd_config_iterator_entry(first); + + if (snd_config_get_id(class, &class_name) < 0) + return NULL; + + class_config_id = tplg_snprintf("Class.%s.%s", class_type, class_name); + if (!class_config_id) + return NULL; + + ret = snd_config_search(tplg_pp->input_cfg, class_config_id, &class_cfg); + if (ret < 0) + SNDERR("No Class definition found for %s\n", class_config_id); + + free(class_config_id); + return class_cfg; +} + +/* find the attribute config by name in the class definition */ +snd_config_t *tplg_class_find_attribute_by_name(struct tplg_pre_processor *tplg_p ATTRIBUTE_UNUSED, + snd_config_t *class, const char *name) +{ + snd_config_t *attr = NULL; + const char *class_id; + char *attr_str; + int ret; + + if (snd_config_get_id(class, &class_id) < 0) + return NULL; + + attr_str = tplg_snprintf("DefineAttribute.%s", name); + if (!attr_str) + return NULL; + + ret = snd_config_search(class, attr_str, &attr); + if (ret < 0) + SNDERR("No definition for attribute '%s' in class '%s'\n", + name, class_id); + + free(attr_str); + return attr; +} + +/* get the name of the attribute that must have a unique value in the object instance */ +const char *tplg_class_get_unique_attribute_name(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *class) +{ + snd_config_t *unique; + const char *unique_name, *class_id; + int ret; + + if (snd_config_get_id(class, &class_id) < 0) + return NULL; + + ret = snd_config_search(class, "attributes.unique", &unique); + if (ret < 0) { + SNDERR("No unique attribute in class '%s'\n", class_id); + return NULL; + } + + if (snd_config_get_string(unique, &unique_name) < 0) { + SNDERR("Invalid name for unique attribute in class '%s'\n", class_id); + return NULL; + } + + return unique_name; +} + +/* get attribute type from the definition */ +snd_config_type_t tplg_class_get_attribute_type(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *attr) +{ + snd_config_t *type; + const char *s; + int ret; + + /* default to integer if no type is given */ + ret = snd_config_search(attr, "type", &type); + if (ret < 0) + return SND_CONFIG_TYPE_INTEGER; + + ret = snd_config_get_string(type, &s); + assert(ret >= 0); + + if (!strcmp(s, "string")) + return SND_CONFIG_TYPE_STRING; + + if (!strcmp(s, "compound")) + return SND_CONFIG_TYPE_COMPOUND; + + if (!strcmp(s, "real")) + return SND_CONFIG_TYPE_REAL; + + if (!strcmp(s, "integer64")) + return SND_CONFIG_TYPE_INTEGER64; + + return SND_CONFIG_TYPE_INTEGER; +} + +/* get token_ref for attribute with name attr_name in the class */ +const char *tplg_class_get_attribute_token_ref(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *class, const char *attr_name) +{ + snd_config_t *attributes, *attr, *token_ref; + const char *token; + int ret; + + ret = snd_config_search(class, "DefineAttribute", &attributes); + if (ret < 0) + return NULL; + + ret = snd_config_search(attributes, attr_name, &attr); + if (ret < 0) + return NULL; + + ret = snd_config_search(attr, "token_ref", &token_ref); + if (ret < 0) + return NULL; + + ret = snd_config_get_string(token_ref, &token); + if (ret < 0) + return NULL; + + return token; +} + +/* convert a valid attribute string value to the corresponding tuple value */ +long tplg_class_attribute_valid_tuple_value(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *class, snd_config_t *attr) +{ + + snd_config_t *attributes, *cfg, *valid, *tuples, *n; + snd_config_iterator_t i, next; + const char *attr_name, *attr_value; + int ret; + + ret = snd_config_get_id(attr, &attr_name); + if (ret < 0) + return -EINVAL; + + ret = snd_config_get_string(attr, &attr_value); + if (ret < 0) + return -EINVAL; + + /* find attribute definition in class */ + ret = snd_config_search(class, "DefineAttribute", &attributes); + if (ret < 0) + return -EINVAL; + + + ret = snd_config_search(attributes, attr_name, &cfg); + if (ret < 0) + return -EINVAL; + + /* check if it has valid values */ + ret = snd_config_search(cfg, "constraints.valid_values", &valid); + if (ret < 0) + return -EINVAL; + + ret = snd_config_search(cfg, "constraints.tuple_values", &tuples); + if (ret < 0) + return -EINVAL; + + /* find and return the tuple value matching the attribute value id */ + snd_config_for_each(i, next, valid) { + const char *s, *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_string(n, &s) < 0) + continue; + if (snd_config_get_id(n, &id) < 0) + continue; + + if (!strcmp(attr_value, s)) { + snd_config_t *tuple; + long tuple_value; + + ret = snd_config_search(tuples, id, &tuple); + if (ret < 0) + return -EINVAL; + + ret = snd_config_get_integer(tuple, &tuple_value); + if (ret < 0) + return ret; + + return tuple_value; + } + } + + return -EINVAL; +} diff --git a/topology/pre-process-dai.c b/topology/pre-process-dai.c new file mode 100644 index 0000000..343ce81 --- /dev/null +++ b/topology/pre-process-dai.c @@ -0,0 +1,138 @@ +/* + Copyright(c) 2021 Intel Corporation + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + The full GNU General Public License is included in this distribution + in the file called LICENSE.GPL. +*/ +#include "aconfig.h" +#include +#include +#include +#include "topology.h" +#include "pre-processor.h" + +int tplg_build_hw_cfg_object(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_cfg, snd_config_t *parent) +{ + snd_config_t *hw_cfg, *obj; + const char *name; + int ret; + + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + name = tplg_object_get_name(tplg_pp, obj); + if (!name) + return -EINVAL; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &hw_cfg, NULL, false); + if (ret < 0) + return ret; + + return tplg_parent_update(tplg_pp, parent, "hw_configs", name); +} + +int tplg_build_fe_dai_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_base_object(tplg_pp, obj_cfg, parent, false); +} + +static int tplg_update_pcm_object(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_cfg, snd_config_t *parent) +{ + snd_config_t *top, *parent_obj, *obj, *dest, *cfg, *pcm, *child; + const char *parent_name, *item_name, *direction; + int ret; + + /* get object name */ + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + item_name = tplg_object_get_name(tplg_pp, obj); + if (!item_name) + return -EINVAL; + + /* get direction */ + ret = snd_config_search(obj, "direction", &cfg); + if (ret < 0) { + SNDERR("no direction attribute in %s\n", item_name); + return ret; + } + + ret = snd_config_get_string(cfg, &direction); + if (ret < 0) { + SNDERR("Invalid direction attribute in %s\n", item_name); + return ret; + } + + /* add to parent section */ + top = tplg_object_get_section(tplg_pp, parent); + if (!top) { + SNDERR("Cannot find parent for %s\n", item_name); + return -EINVAL; + } + + parent_obj = tplg_object_get_instance_config(tplg_pp, parent); + + /* get parent name. if parent has no name, skip adding config */ + parent_name = tplg_object_get_name(tplg_pp, parent_obj); + if (!parent_name) + return 0; + + /* find parent config with name */ + dest = tplg_find_config(top, parent_name); + if (!dest) { + SNDERR("Cannot find parent section %s\n", parent_name); + return -EINVAL; + } + + ret = snd_config_search(dest, "pcm", &pcm); + if (ret < 0) { + ret = tplg_config_make_add(&pcm, "pcm", SND_CONFIG_TYPE_COMPOUND, dest); + if (ret < 0) { + SNDERR("Error creating pcm config in %s\n", parent_name); + return ret; + } + } + + ret = snd_config_search(pcm, direction, &cfg); + if (ret >= 0) { + SNDERR("pcm.%s exists already in %s\n", direction, parent_name); + return -EEXIST; + } + + ret = tplg_config_make_add(&cfg, direction, SND_CONFIG_TYPE_COMPOUND, pcm); + + if (ret >= 0) + ret = tplg_config_make_add(&child, "capabilities", SND_CONFIG_TYPE_STRING, cfg); + + if (ret >= 0) + ret = snd_config_set_string(child, item_name); + + return ret; +} + +int tplg_build_pcm_caps_object(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_cfg, snd_config_t *parent) +{ + snd_config_t *caps; + int ret; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &caps, NULL, false); + if (ret < 0) + return ret; + + /* add pcm capabilities to parent */ + return tplg_update_pcm_object(tplg_pp, obj_cfg, parent); +} diff --git a/topology/pre-process-dapm.c b/topology/pre-process-dapm.c new file mode 100644 index 0000000..78944a6 --- /dev/null +++ b/topology/pre-process-dapm.c @@ -0,0 +1,451 @@ +/* + Copyright(c) 2021 Intel Corporation + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + The full GNU General Public License is included in this distribution + in the file called LICENSE.GPL. +*/ +#include +#include +#include +#include "topology.h" +#include "pre-processor.h" + +int tplg_build_base_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent, bool skip_name) +{ + snd_config_t *top, *parent_obj, *cfg, *dest; + const char *parent_name; + + /* find parent section config */ + top = tplg_object_get_section(tplg_pp, parent); + if (!top) + return -EINVAL; + + parent_obj = tplg_object_get_instance_config(tplg_pp, parent); + + /* get parent name */ + parent_name = tplg_object_get_name(tplg_pp, parent_obj); + if (!parent_name) + return 0; + + /* find parent config with name */ + dest = tplg_find_config(top, parent_name); + if (!dest) { + SNDERR("Cannot find parent config %s\n", parent_name); + return -EINVAL; + } + + /* build config from template and add to parent */ + return tplg_build_object_from_template(tplg_pp, obj_cfg, &cfg, dest, skip_name); +} + +int tplg_build_scale_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_base_object(tplg_pp, obj_cfg, parent, true); +} + +int tplg_build_ops_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_base_object(tplg_pp, obj_cfg, parent, false); +} + +int tplg_build_channel_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_base_object(tplg_pp, obj_cfg, parent, false); +} + +int tplg_build_text_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + snd_config_t *cfg; + const char *name; + int ret; + + cfg = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + name = tplg_object_get_name(tplg_pp, cfg); + if (!name) + return -EINVAL; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &cfg, NULL, false); + if (ret < 0) + return ret; + + return tplg_parent_update(tplg_pp, parent, "texts", name); +} + +int tplg_build_tlv_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + snd_config_t *cfg; + const char *name; + int ret; + + cfg = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + name = tplg_object_get_name(tplg_pp, cfg); + if (!name) + return -EINVAL; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &cfg, NULL, false); + if (ret < 0) + return ret; + + return tplg_parent_update(tplg_pp, parent, "tlv", name); +} + +static int tplg_build_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent, char *type) +{ + snd_config_t *cfg, *obj; + const char *name; + int ret; + + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + /* get control name */ + ret = snd_config_search(obj, "name", &cfg); + if (ret < 0) + return 0; + + ret = snd_config_get_string(cfg, &name); + if (ret < 0) + return ret; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &cfg, NULL, false); + if (ret < 0) + return ret; + + ret = tplg_add_object_data(tplg_pp, obj_cfg, cfg, NULL); + if (ret < 0) + SNDERR("Failed to add data section for %s\n", name); + + return tplg_parent_update(tplg_pp, parent, type, name); +} + +int tplg_build_mixer_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_control(tplg_pp, obj_cfg, parent, "mixer"); +} + +int tplg_build_bytes_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_control(tplg_pp, obj_cfg, parent, "bytes"); +} + +int tplg_build_enum_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + return tplg_build_control(tplg_pp, obj_cfg, parent, "enum"); +} + +/* + * Widget names for pipeline endpoints can be of the following type: + * "class. ex: pga.0.1, buffer.1.1 etc + * Optionally, the index argument for a widget can be omitted and will be substituted with + * the index from the route: ex: pga..0, host..playback etc + */ +static int tplg_pp_get_widget_name(struct tplg_pre_processor *tplg_pp, + const char *string, long index, char **widget) +{ + snd_config_iterator_t i, next; + snd_config_t *temp_cfg, *child, *class_cfg, *n; + char *class_name, *args, *widget_name; + int ret; + + /* get class name */ + args = strchr(string, '.'); + if (!args) { + SNDERR("Error getting class name for %s\n", string); + return -EINVAL; + } + + class_name = calloc(1, strlen(string) - strlen(args) + 1); + if (!class_name) + return -ENOMEM; + + snprintf(class_name, strlen(string) - strlen(args) + 1, "%s", string); + + /* create config with Widget class type */ + ret = snd_config_make(&temp_cfg, "Widget", SND_CONFIG_TYPE_COMPOUND); + if (ret < 0) { + free(class_name); + return ret; + } + + /* create config with class name and add it to the Widget config */ + ret = tplg_config_make_add(&child, class_name, SND_CONFIG_TYPE_COMPOUND, temp_cfg); + if (ret < 0) { + free(class_name); + return ret; + } + + /* get class definition for widget */ + class_cfg = tplg_class_lookup(tplg_pp, temp_cfg); + snd_config_delete(temp_cfg); + if (!class_cfg) { + free(class_name); + return -EINVAL; + } + + /* get constructor for class */ + ret = snd_config_search(class_cfg, "attributes.constructor", &temp_cfg); + if (ret < 0) { + SNDERR("No arguments in class for widget %s\n", string); + free(class_name); + return ret; + } + + widget_name = strdup(class_name); + free(class_name); + if (!widget_name) + return -ENOMEM; + + /* construct widget name using the constructor argument values */ + snd_config_for_each(i, next, temp_cfg) { + const char *id; + char *arg, *remaining, *temp; + + n = snd_config_iterator_entry(i); + if (snd_config_get_string(n, &id) < 0) + continue; + + if (!args) { + SNDERR("insufficient arugments for widget %s\n", string); + ret = -EINVAL; + goto err; + } + + remaining = strchr(args + 1, '.'); + if (remaining) { + arg = calloc(1, strlen(args + 1) - strlen(remaining) + 1); + if (!arg) { + ret = -ENOMEM; + goto err; + } + snprintf(arg, strlen(args + 1) - strlen(remaining) + 1, "%s", args + 1); + } else { + arg = calloc(1, strlen(args + 1) + 1); + if (!arg) { + ret = -ENOMEM; + goto err; + } + + snprintf(arg, strlen(args + 1) + 1, "%s", args + 1); + } + + /* if no index provided, substitue with route index */ + if (!strcmp(arg, "") && !strcmp(id, "index")) { + free(arg); + arg = tplg_snprintf("%ld", index); + if (!arg) { + ret = -ENOMEM; + free(arg); + goto err; + } + } + + temp = tplg_snprintf("%s.%s", widget_name, arg); + if (!temp) { + ret = -ENOMEM; + free(arg); + goto err; + } + + free(widget_name); + widget_name = temp; + free(arg); + if (remaining) + args = remaining; + else + args = NULL; + } + + *widget = widget_name; + return 0; + +err: + free(widget_name); + return ret; +} + +int tplg_build_dapm_route_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + snd_config_t *top, *obj, *cfg, *route, *child, *parent_obj; + const char *name, *wname; + const char *parent_name = "Endpoint"; + char *src_widget_name, *sink_widget_name, *line_str, *route_name; + const char *control = ""; + long index = 0; + int ret; + + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + ret = snd_config_get_id(obj, &name); + if (ret < 0) + return -EINVAL; + + /* endpoint connections at the top-level conf have no parent */ + if (parent) { + parent_obj = tplg_object_get_instance_config(tplg_pp, parent); + + ret = snd_config_get_id(parent_obj, &parent_name); + if (ret < 0) + return -EINVAL; + } + + tplg_pp_debug("Building DAPM route object: '%s' ...", name); + + ret = snd_config_search(tplg_pp->output_cfg, "SectionGraph", &top); + if (ret < 0) { + ret = tplg_config_make_add(&top, "SectionGraph", + SND_CONFIG_TYPE_COMPOUND, tplg_pp->output_cfg); + if (ret < 0) { + SNDERR("Error creating 'SectionGraph' config\n"); + return ret; + } + } + + /* get route index */ + ret = snd_config_search(obj, "index", &cfg); + if (ret >= 0) { + ret = snd_config_get_integer(cfg, &index); + if (ret < 0) { + SNDERR("Invalid index route %s\n", name); + return ret; + } + } + + /* get source widget name */ + ret = snd_config_search(obj, "source", &cfg); + if (ret < 0) { + SNDERR("No source for route %s\n", name); + return ret; + } + + ret = snd_config_get_string(cfg, &wname); + if (ret < 0) { + SNDERR("Invalid name for source in route %s\n", name); + return ret; + } + + ret = tplg_pp_get_widget_name(tplg_pp, wname, index, &src_widget_name); + if (ret < 0) { + SNDERR("error getting widget name for %s\n", wname); + return ret; + } + + /* get sink widget name */ + ret = snd_config_search(obj, "sink", &cfg); + if (ret < 0) { + SNDERR("No sink for route %s\n", name); + free(src_widget_name); + return ret; + } + + ret = snd_config_get_string(cfg, &wname); + if (ret < 0) { + SNDERR("Invalid name for sink in route %s\n", name); + free(src_widget_name); + return ret; + } + + ret = tplg_pp_get_widget_name(tplg_pp, wname, index, &sink_widget_name); + if (ret < 0) { + SNDERR("error getting widget name for %s\n", wname); + free(src_widget_name); + return ret; + } + + /* get control name */ + ret = snd_config_search(obj, "control", &cfg); + if (ret >= 0) { + ret = snd_config_get_string(cfg, &control); + if (ret < 0) { + SNDERR("Invalid control name for route %s\n", name); + goto err; + } + } + + /* add route */ + route_name = tplg_snprintf("%s.%s", parent_name, name); + if (!route_name) { + ret = -ENOMEM; + goto err; + } + + ret = snd_config_make(&route, route_name, SND_CONFIG_TYPE_COMPOUND); + free(route_name); + if (ret < 0) { + SNDERR("Error creating route config for %s %d\n", name, ret); + goto err; + } + + ret = snd_config_add(top, route); + if (ret < 0) { + SNDERR("Error adding route config for %s %d\n", name, ret); + goto err; + } + + /* add index */ + ret = tplg_config_make_add(&child, "index", SND_CONFIG_TYPE_INTEGER, route); + if (ret < 0) { + SNDERR("Error creating index config for %s\n", name); + goto err; + } + + ret = snd_config_set_integer(child, index); + if (ret < 0) { + SNDERR("Error setting index config for %s\n", name); + goto err; + } + + /* add lines */ + ret = tplg_config_make_add(&cfg, "lines", SND_CONFIG_TYPE_COMPOUND, route); + if (ret < 0) { + SNDERR("Error creating lines config for %s\n", name); + goto err; + } + + /* add route string */ + ret = tplg_config_make_add(&child, "0", SND_CONFIG_TYPE_STRING, cfg); + if (ret < 0) { + SNDERR("Error creating lines config for %s\n", name); + goto err; + } + + line_str = tplg_snprintf("%s, %s, %s", sink_widget_name, control, src_widget_name); + if (!line_str) { + ret = -ENOMEM; + goto err; + } + + /* set the line string */ + ret = snd_config_set_string(child, line_str); + free(line_str); + if (ret < 0) + SNDERR("Error creating lines config for %s\n", name); +err: + free(src_widget_name); + free(sink_widget_name); + return ret; +} diff --git a/topology/pre-process-external.h b/topology/pre-process-external.h new file mode 100644 index 0000000..9d930ef --- /dev/null +++ b/topology/pre-process-external.h @@ -0,0 +1,39 @@ +/* + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#ifndef __PRE_PROCESS_EXTERNAL_H +#define __PRE_PROCESS_EXTERNAL_H + +#define PROCESS_FUNC_PREFIX "_snd_topology_" +#define PROCESS_FUNC_POSTFIX "_process" +#define PROCESS_LIB_PREFIX "libalsatplg_module_" +#define PROCESS_LIB_POSTFIX ".so" + +/** + * Define the object entry for external pre-process plugins + */ +#define SND_TOPOLOGY_PLUGIN_ENTRY(name) _snd_topology_##name##_process + +/** + * Define the plugin + */ +#define SND_TOPOLOGY_PLUGIN_DEFINE_FUNC(plugin) \ + __attribute__ ((visibility ("default"))) \ + int SND_TOPOLOGY_PLUGIN_ENTRY(plugin) (snd_config_t *input, snd_config_t *output) + +typedef int (*plugin_pre_process)(snd_config_t *input, snd_config_t *output); + +#endif /* __PRE_PROCESS_EXTERNAL_H */ diff --git a/topology/pre-process-object.c b/topology/pre-process-object.c new file mode 100644 index 0000000..de088a6 --- /dev/null +++ b/topology/pre-process-object.c @@ -0,0 +1,2094 @@ +/* + Copyright(c) 2021 Intel Corporation + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + The full GNU General Public License is included in this distribution + in the file called LICENSE.GPL. +*/ +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include "gettext.h" +#include "topology.h" +#include "pre-processor.h" + +/* Parse VendorToken object, create the "SectionVendorToken" and save it */ +int tplg_build_vendor_token_object(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_cfg, + snd_config_t *parent ATTRIBUTE_UNUSED) +{ + snd_config_iterator_t i, next; + snd_config_t *vtop, *n, *obj; + const char *name; + int ret; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &vtop, NULL, false); + if (ret < 0) + return ret; + + ret = snd_config_get_id(vtop, &name); + if (ret < 0) + return ret; + + /* add the tuples */ + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + snd_config_for_each(i, next, obj) { + snd_config_t *dst; + const char *id; + + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) + continue; + + if (!strcmp(id, "name")) + continue; + + ret = snd_config_copy(&dst, n); + if (ret < 0) { + SNDERR("Error copying config node %s for '%s'\n", id, name); + return ret; + } + + ret = snd_config_add(vtop, dst); + if (ret < 0) { + snd_config_delete(dst); + SNDERR("Error adding vendortoken %s for %s\n", id, name); + return ret; + } + } + + return ret; +} + +int tplg_parent_update(struct tplg_pre_processor *tplg_pp, snd_config_t *parent, + const char *section_name, const char *item_name) +{ + snd_config_iterator_t i, next; + snd_config_t *child, *cfg, *top, *item_config, *n; + const char *parent_name; + char *item_id; + int ret, id = 0; + + /* Nothing to do if parent is NULL */ + if (!parent) + return 0; + + child = tplg_object_get_instance_config(tplg_pp, parent); + ret = snd_config_search(child, "name", &cfg); + if (ret < 0) { + ret = snd_config_get_id(child, &parent_name); + if (ret < 0) { + SNDERR("No name config for parent\n"); + return ret; + } + } else { + ret = snd_config_get_string(cfg, &parent_name); + if (ret < 0) { + SNDERR("Invalid name for parent\n"); + return ret; + } + } + + top = tplg_object_get_section(tplg_pp, parent); + if (!top) + return -EINVAL; + + /* get config with name */ + cfg = tplg_find_config(top, parent_name); + if (!cfg) + return ret; + + /* get section config */ + if (!strcmp(section_name, "tlv") || !strcmp(section_name, "texts")) { + /* set tlv name if config exists already */ + ret = snd_config_search(cfg, section_name, &item_config); + if (ret < 0) { + ret = tplg_config_make_add(&item_config, section_name, + SND_CONFIG_TYPE_STRING, cfg); + if (ret < 0) { + SNDERR("Error creating section config widget %s for %s\n", + section_name, parent_name); + return ret; + } + } + + return snd_config_set_string(item_config, item_name); + } + + ret = snd_config_search(cfg, section_name, &item_config); + if (ret < 0) { + ret = tplg_config_make_add(&item_config, section_name, + SND_CONFIG_TYPE_COMPOUND, cfg); + if (ret < 0) { + SNDERR("Error creating section config widget %s for %s\n", + section_name, parent_name); + return ret; + } + } + + snd_config_for_each(i, next, item_config) { + const char *name; + + n = snd_config_iterator_entry(i); + if (snd_config_get_string(n, &name) < 0) + continue; + + /* item already exists */ + if (!strcmp(name, item_name)) + return 0; + id++; + } + + /* add new item */ + item_id = tplg_snprintf("%d", id); + if (!item_id) + return -ENOMEM; + + ret = snd_config_make(&cfg, item_id, SND_CONFIG_TYPE_STRING); + free(item_id); + if (ret < 0) + return ret; + + ret = snd_config_set_string(cfg, item_name); + if (ret < 0) + return ret; + + ret = snd_config_add(item_config, cfg); + if (ret < 0) + snd_config_delete(cfg); + + return ret; +} + +/* Parse data object, create the "SectionData" and save it. Only "bytes" data supported for now */ +int tplg_build_data_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + snd_config_t *dtop; + const char *name; + int ret; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &dtop, NULL, false); + if (ret < 0) + return ret; + + ret = snd_config_get_id(dtop, &name); + if (ret < 0) + return ret; + + return tplg_parent_update(tplg_pp, parent, "data", name); +} + +static int tplg_create_config_template(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t **template, + const struct config_template_items *items) +{ + snd_config_t *top, *child; + int ret, i; + + ret = snd_config_make(&top, "template", SND_CONFIG_TYPE_COMPOUND); + if (ret < 0) + return ret; + + /* add integer configs */ + for (i = 0; i < MAX_CONFIGS_IN_TEMPLATE; i++) + if (items->int_config_ids[i]) { + ret = tplg_config_make_add(&child, items->int_config_ids[i], + SND_CONFIG_TYPE_INTEGER, top); + if (ret < 0) + goto err; + } + + /* add string configs */ + for (i = 0; i < MAX_CONFIGS_IN_TEMPLATE; i++) + if (items->string_config_ids[i]) { + ret = tplg_config_make_add(&child, items->string_config_ids[i], + SND_CONFIG_TYPE_STRING, top); + if (ret < 0) + goto err; + } + + /* add compound configs */ + for (i = 0; i < MAX_CONFIGS_IN_TEMPLATE; i++) { + if (items->compound_config_ids[i]) { + ret = tplg_config_make_add(&child, items->compound_config_ids[i], + SND_CONFIG_TYPE_COMPOUND, top); + if (ret < 0) + goto err; + } + } + +err: + if (ret < 0) { + snd_config_delete(top); + return ret; + } + + *template = top; + return ret; +} + +static void tplg_attribute_print_valid_values(snd_config_t *valid_values, const char *name) +{ + snd_config_iterator_t i, next; + snd_config_t *n; + + SNDERR("valid values for attribute %s are:\n", name); + + snd_config_for_each(i, next, valid_values) { + const char *s, *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (snd_config_get_string(n, &s) < 0) + continue; + + SNDERR("%s", s); + } +} + +/* check is attribute value belongs in the set of valid values */ +static bool tplg_is_attribute_valid_value(snd_config_t *valid_values, const char *value) +{ + snd_config_iterator_t i, next; + snd_config_t *n; + + snd_config_for_each(i, next, valid_values) { + const char *s, *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (snd_config_get_string(n, &s) < 0) + continue; + + if (!strcmp(value, s)) + return true; + } + + return false; +} + +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION +static int pre_process_find_variable(snd_config_t **dst, const char *str, snd_config_t *config); + +/* check is attribute value belongs in the set of valid values after expanding the valid values */ +static bool tplg_is_attribute_valid_expanded_value(struct tplg_pre_processor *tplg_pp, + snd_config_t *valid_values, int value) +{ + snd_config_iterator_t i, next; + snd_config_t *n, *var, *conf_defines; + int ret; + + ret = snd_config_search(tplg_pp->input_cfg, "Define", &conf_defines); + if (ret < 0) + return false; + + snd_config_for_each(i, next, valid_values) { + const char *s, *id; + long v; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (snd_config_get_string(n, &s) < 0) + continue; + + if (s[0] != '$') + continue; + + /* find the variable definition */ + ret = pre_process_find_variable(&var, s + 1, conf_defines); + if (ret < 0) { + SNDERR("No definition for variable %s\n", s + 1); + return false; + } + + if (snd_config_get_integer(var, &v) < 0) { + SNDERR("Invalid definition for %s\n", s); + snd_config_delete(var); + return false; + } + + snd_config_delete(var); + + if (value == v) + return true; + } + + return false; +} +#endif + +/* check if attribute value passes the min/max value constraints */ +static bool tplg_object_is_attribute_min_max_valid(snd_config_t *attr, snd_config_t *obj_attr, + bool min_check) +{ + snd_config_type_t type = snd_config_get_type(obj_attr); + snd_config_t *valid; + const char *attr_name; + int ret; + + if (snd_config_get_id(attr, &attr_name) < 0) + return false; + + if (min_check) { + ret = snd_config_search(attr, "constraints.min", &valid); + if (ret < 0) + return true; + } else { + ret = snd_config_search(attr, "constraints.max", &valid); + if (ret < 0) + return true; + } + + switch(type) { + case SND_CONFIG_TYPE_INTEGER: + { + long v, m; + + if (snd_config_get_integer(valid, &m) < 0) + return true; + + if (snd_config_get_integer(obj_attr, &v) < 0) + return false; + + if (min_check) { + if (v < m) { + SNDERR("attribute '%s' value: %ld is less than min value: %d\n", + attr_name, v, m); + return false; + } + } else { + if (v > m) { + SNDERR("attribute '%s' value: %ld is greater than max value: %d\n", + attr_name, v, m); + return false; + } + } + + return true; + } + case SND_CONFIG_TYPE_INTEGER64: + { + long long v; + long m; + + if (snd_config_get_integer(valid, &m) < 0) + return true; + + if (snd_config_get_integer64(obj_attr, &v) < 0) + return false; + + if (min_check) { + if (v < m) { + SNDERR("attribute '%s' value: %ld is less than min value: %d\n", + attr_name, v, m); + return false; + } + } else { + if (v > m) { + SNDERR("attribute '%s' value: %ld is greater than max value: %d\n", + attr_name, v, m); + return false; + } + } + + return true; + } + default: + break; + } + + return false; +} + +/* check for min/max and valid value constraints */ +static bool tplg_object_is_attribute_valid(struct tplg_pre_processor *tplg_pp, + snd_config_t *attr, snd_config_t *obj_attr) +{ + snd_config_iterator_t i, next; + snd_config_t *valid, *n; + snd_config_type_t type; + const char *attr_name, *obj_value; + int ret; + + if (snd_config_get_id(attr, &attr_name) < 0) + return false; + + type = snd_config_get_type(obj_attr); + + /* check if attribute has valid values */ + ret = snd_config_search(attr, "constraints.valid_values", &valid); + if (ret < 0) + goto min_max_check; + + switch(type) { + case SND_CONFIG_TYPE_STRING: + if (snd_config_get_string(obj_attr, &obj_value) < 0) + return false; + if (!tplg_is_attribute_valid_value(valid, obj_value)) { + tplg_attribute_print_valid_values(valid, attr_name); + return false; + } + return true; + case SND_CONFIG_TYPE_COMPOUND: + snd_config_for_each(i, next, obj_attr) { + const char *s, *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (snd_config_get_string(n, &s) < 0) + continue; + + if (!tplg_is_attribute_valid_value(valid, s)) { + tplg_attribute_print_valid_values(valid, attr_name); + return false; + } + } + return true; +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + case SND_CONFIG_TYPE_INTEGER: + { + long v; + + if (snd_config_get_integer(obj_attr, &v) < 0) + return false; + + if (!tplg_is_attribute_valid_expanded_value(tplg_pp, valid, v)) { + tplg_attribute_print_valid_values(valid, attr_name); + return false; + } + return true; + } +#endif + default: + break; + } + + return false; + +min_max_check: + if (!tplg_object_is_attribute_min_max_valid(attr, obj_attr, true)) + return false; + + return tplg_object_is_attribute_min_max_valid(attr, obj_attr, false); +} + +/* get object's name attribute value */ +const char *tplg_object_get_name(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *object) +{ + snd_config_t *cfg; + const char *name; + int ret; + + ret = snd_config_search(object, "name", &cfg); + if (ret < 0) + return NULL; + + ret = snd_config_get_string(cfg, &name); + if (ret < 0) + return NULL; + + return name; +} + +/* look up the instance of object in a config */ +static snd_config_t *tplg_object_lookup_in_config(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *class, const char *type, + const char *class_name, const char *id) +{ + snd_config_t *obj_cfg = NULL; + char *config_id; + + config_id = tplg_snprintf("Object.%s.%s.%s", type, class_name, id); + if (!config_id) + return NULL; + + if (snd_config_search(class, config_id, &obj_cfg) < 0) + return NULL; + free(config_id); + return obj_cfg; +} + +static int tplg_pp_add_object_tuple_section(struct tplg_pre_processor *tplg_pp, + snd_config_t *class_cfg, + snd_config_t *attr, char *data_name, + const char *token_ref, const char *array_name) +{ + snd_config_t *top, *tuple_cfg, *child, *cfg, *new; + const char *id; + char *token, *type, *str; + long tuple_value; + int ret; + + tplg_pp_debug("Building vendor tuples section: '%s' ...", data_name); + + ret = snd_config_search(tplg_pp->output_cfg, "SectionVendorTuples", &top); + if (ret < 0) { + ret = tplg_config_make_add(&top, "SectionVendorTuples", + SND_CONFIG_TYPE_COMPOUND, tplg_pp->output_cfg); + if (ret < 0) { + SNDERR("Error creating SectionVendorTuples config\n"); + return ret; + } + } + + type = strchr(token_ref, '.'); + if(!type) { + SNDERR("Error getting type for %s\n", token_ref); + return -EINVAL; + } + + token = calloc(1, strlen(token_ref) - strlen(type) + 1); + if (!token) + return -ENOMEM; + snprintf(token, strlen(token_ref) - strlen(type) + 1, "%s", token_ref); + + if (!array_name) + str = strdup(type + 1); + else + str = tplg_snprintf("%s.%s", type + 1, array_name); + if (!str) { + ret = -ENOMEM; + goto free; + } + + tuple_cfg = tplg_find_config(top, data_name); + if (!tuple_cfg) { + /* add new SectionVendorTuples */ + ret = tplg_config_make_add(&tuple_cfg, data_name, SND_CONFIG_TYPE_COMPOUND, top); + if (ret < 0) { + SNDERR("Error creating new vendor tuples config %s\n", data_name); + goto err; + } + + ret = tplg_config_make_add(&child, "tokens", SND_CONFIG_TYPE_STRING, + tuple_cfg); + if (ret < 0) { + SNDERR("Error creating tokens config for '%s'\n", data_name); + goto err; + } + + ret = snd_config_set_string(child, token); + if (ret < 0) { + SNDERR("Error setting tokens config for '%s'\n", data_name); + goto err; + } + + ret = tplg_config_make_add(&child, "tuples", SND_CONFIG_TYPE_COMPOUND, + tuple_cfg); + if (ret < 0) { + SNDERR("Error creating tuples config for '%s'\n", data_name); + goto err; + } + + ret = tplg_config_make_add(&cfg, str, SND_CONFIG_TYPE_COMPOUND, + child); + if (ret < 0) { + SNDERR("Error creating tuples type config for '%s'\n", data_name); + goto err; + } + } else { + snd_config_t *tuples_cfg; + + ret = snd_config_search(tuple_cfg, "tuples" , &tuples_cfg); + if (ret < 0) { + SNDERR("can't find tuples config in %s\n", data_name); + goto err; + } + + cfg = tplg_find_config(tuples_cfg, str); + if (!cfg) { + ret = tplg_config_make_add(&cfg, str, SND_CONFIG_TYPE_COMPOUND, + tuples_cfg); + if (ret < 0) { + SNDERR("Error creating tuples config for '%s' and type name %s\n", data_name, str); + goto err; + } + } + } + + ret = snd_config_get_id(attr, &id); + if (ret < 0) + goto err; + + /* tuple exists already? */ + ret = snd_config_search(cfg, id, &child); + if (ret >=0) + goto err; + + /* add attribute to tuples */ + tuple_value = tplg_class_attribute_valid_tuple_value(tplg_pp, class_cfg, attr); + if (tuple_value < 0) { + /* just copy attribute cfg as is */ + ret = snd_config_copy(&new, attr); + if (ret < 0) { + SNDERR("can't copy attribute for %s\n", data_name); + goto err; + } + } else { + ret = snd_config_make(&new, id, SND_CONFIG_TYPE_INTEGER); + if (ret < 0) + goto err; + + ret = snd_config_set_integer(new, tuple_value); + if (ret < 0) + goto err; + } + + ret = snd_config_add(cfg, new); +err: + free(str); +free: + free(token); + return ret; +} + +static int tplg_pp_add_object_data_section(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_data, char *data_name) +{ + snd_config_iterator_t i, next; + snd_config_t *top, *data_cfg, *child; + char *data_id; + int ret, id = 0; + + ret = snd_config_search(tplg_pp->output_cfg, "SectionData", &top); + if (ret < 0) { + ret = tplg_config_make_add(&top, "SectionData", SND_CONFIG_TYPE_COMPOUND, + tplg_pp->output_cfg); + if (ret < 0) { + SNDERR("Failed to add SectionData\n"); + return ret; + } + } + + /* nothing to do if data section already exists */ + data_cfg = tplg_find_config(top, data_name); + if (data_cfg) + return 0; + + tplg_pp_debug("Building data section %s ...", data_name); + + /* add new SectionData */ + ret = tplg_config_make_add(&data_cfg, data_name, SND_CONFIG_TYPE_COMPOUND, top); + if (ret < 0) + return ret; + + ret = tplg_config_make_add(&child, "tuples", SND_CONFIG_TYPE_STRING, data_cfg); + if (ret < 0) { + SNDERR("error adding data ref for %s\n", data_name); + return ret; + } + + ret = snd_config_set_string(child, data_name); + if (ret < 0) { + SNDERR("error setting tuples ref for %s\n", data_name); + return ret; + } + + /* add data item to object */ + snd_config_for_each(i, next, obj_data) + id++; + + data_id = tplg_snprintf("%d", id); + if (!data_id) + return -ENOMEM; + + ret = tplg_config_make_add(&child, data_id, SND_CONFIG_TYPE_STRING, obj_data); + free(data_id); + if (ret < 0) { + SNDERR("error adding data ref %s\n", data_name); + return ret; + } + + return snd_config_set_string(child, data_name); +} + +int tplg_add_object_data(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *top, const char *array_name) +{ + snd_config_iterator_t i, next; + snd_config_t *data_cfg, *class_cfg, *n, *obj; + const char *object_id; + int ret; + + if (snd_config_get_id(top, &object_id) < 0) + return 0; + + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + class_cfg = tplg_class_lookup(tplg_pp, obj_cfg); + if (!class_cfg) + return -EINVAL; + + /* add data config to top */ + ret = snd_config_search(top, "data", &data_cfg); + if (ret < 0) { + ret = tplg_config_make_add(&data_cfg, "data", SND_CONFIG_TYPE_COMPOUND, top); + if (ret < 0) { + SNDERR("error creating data config for %s\n", object_id); + return ret; + } + } + + /* add data items to object's data section */ + snd_config_for_each(i, next, obj) { + const char *id, *token; + char *data_cfg_name; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + token = tplg_class_get_attribute_token_ref(tplg_pp, class_cfg, id); + if (!token) + continue; + + data_cfg_name = tplg_snprintf("%s.%s", object_id, token); + if (!data_cfg_name) + return -ENOMEM; + + ret = tplg_pp_add_object_data_section(tplg_pp, data_cfg, data_cfg_name); + if (ret < 0) { + SNDERR("Failed to add data section %s\n", data_cfg_name); + free(data_cfg_name); + return ret; + } + + ret = tplg_pp_add_object_tuple_section(tplg_pp, class_cfg, n, data_cfg_name, + token, array_name); + if (ret < 0) { + SNDERR("Failed to add data section %s\n", data_cfg_name); + free(data_cfg_name); + return ret; + } + free(data_cfg_name); + } + + return 0; +} + +/* search for all template configs in the source config and copy them to the destination */ +static int tplg_object_add_attributes(snd_config_t *dst, snd_config_t *template, + snd_config_t *src) +{ + snd_config_iterator_t i, next; + snd_config_t *n; + int ret; + + snd_config_for_each(i, next, template) { + snd_config_t *attr, *new; + const char *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + ret = snd_config_search(src, id, &attr); + if (ret < 0) + continue; + + /* skip if attribute is already set */ + ret = snd_config_search(dst, id, &new); + if (ret >= 0) + continue; + + ret = snd_config_copy(&new, attr); + if (ret < 0) { + SNDERR("failed to copy attribute %s\n", id); + return ret; + } + + ret = snd_config_add(dst, new); + if (ret < 0) { + snd_config_delete(new); + SNDERR("failed to add attribute %s\n", id); + return ret; + } + } + + return 0; +} + +static const struct build_function_map *tplg_object_get_map(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj); + +/* Add object attributes to the private data of the parent object config */ +static int tplg_build_parent_data(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent) +{ + snd_config_t *obj, *parent_obj, *section_cfg, *top; + const struct build_function_map *map; + const char *id, *parent_id; + int ret; + + /* nothing to do if parent is NULL */ + if (!parent) + return 0; + + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + parent_obj = tplg_object_get_instance_config(tplg_pp, parent); + + /* get object ID */ + if (snd_config_get_id(obj, &id) < 0) { + SNDERR("Invalid ID for object\n"); + return -EINVAL; + } + + /* get parent object name or ID */ + parent_id = tplg_object_get_name(tplg_pp, parent_obj); + if (!parent_id) { + ret = snd_config_get_id(parent_obj, &parent_id); + if (ret < 0) { + SNDERR("Invalid ID for parent of object %s\n", id); + return ret; + } + } + + map = tplg_object_get_map(tplg_pp, parent); + if (!map) { + SNDERR("Parent object %s not supported\n", parent_id); + return -EINVAL; + } + + /* find parent config with ID */ + ret = snd_config_search(tplg_pp->output_cfg, map->section_name, §ion_cfg); + if (ret < 0) { + SNDERR("No SectionBE found\n"); + return ret; + } + + top = tplg_find_config(section_cfg, parent_id); + if (!top) { + SNDERR("SectionBE %s not found\n", parent_id); + return -EINVAL; + } + + return tplg_add_object_data(tplg_pp, obj_cfg, top, id); +} + +/* + * Function to create a new "section" config based on the template. The new config will be + * added to the output_cfg or the top_config input parameter. + */ +int tplg_build_object_from_template(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t **wtop, snd_config_t *top_config, + bool skip_name) +{ + snd_config_t *top, *template, *obj; + const struct build_function_map *map; + const char *object_name; + int ret; + + /* look up object map */ + map = tplg_object_get_map(tplg_pp, obj_cfg); + if (!map) { + SNDERR("unknown object type or class name\n"); + return -EINVAL; + } + + obj = tplg_object_get_instance_config(tplg_pp, obj_cfg); + + /* look up or create the corresponding section config for object */ + if (!top_config) + top_config = tplg_pp->output_cfg; + + ret = snd_config_search(top_config, map->section_name, &top); + if (ret < 0) { + ret = tplg_config_make_add(&top, map->section_name, SND_CONFIG_TYPE_COMPOUND, + top_config); + if (ret < 0) { + SNDERR("Error creating %s config\n", map->section_name); + return ret; + } + } + + /* get object name */ + object_name = tplg_object_get_name(tplg_pp, obj); + if (!object_name) { + ret = snd_config_get_id(obj, &object_name); + if (ret < 0) { + SNDERR("Invalid ID for %s\n", map->section_name); + return ret; + } + } + + tplg_pp_debug("Building object: '%s' ...", object_name); + + /* create and add new object config with name, if needed */ + if (skip_name) { + *wtop = top; + } else { + *wtop = tplg_find_config(top, object_name); + if (*wtop) + goto template; + + ret = tplg_config_make_add(wtop, object_name, SND_CONFIG_TYPE_COMPOUND, + top); + if (ret < 0) { + SNDERR("Error creating config for %s\n", object_name); + return ret; + } + } + +template: + /* create template config */ + if (!map->template_items) + return 0; + + ret = tplg_create_config_template(tplg_pp, &template, map->template_items); + if (ret < 0) { + SNDERR("Error creating template config for %s\n", object_name); + return ret; + } + + /* update section config based on template and the attribute values in the object */ + ret = tplg_object_add_attributes(*wtop, template, obj); + snd_config_delete(template); + if (ret < 0) + SNDERR("Error adding attributes for object '%s'\n", object_name); + + return ret; +} + +static int tplg_build_generic_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent ATTRIBUTE_UNUSED) +{ + snd_config_t *wtop; + const char *name; + int ret; + + ret = tplg_build_object_from_template(tplg_pp, obj_cfg, &wtop, NULL, false); + if (ret < 0) + return ret; + + ret = snd_config_get_id(wtop, &name); + if (ret < 0) + return ret; + + ret = tplg_add_object_data(tplg_pp, obj_cfg, wtop, NULL); + if (ret < 0) + SNDERR("Failed to add data section for %s\n", name); + + return ret; +} + +const struct config_template_items pcm_caps_config = { + .int_config_ids = {"rate_min", "rate_max", "channels_min", "channels_max", "periods_min", + "periods_max", "period_size_min", "period_size_max", "buffer_size_min", + "buffer_size_max", "sig_bits"}, + .string_config_ids = {"formats", "rates"}, +}; + +const struct config_template_items fe_dai_config = { + .int_config_ids = {"id"}, +}; + +const struct config_template_items hwcfg_config = { + .int_config_ids = {"id", "bclk_freq", "bclk_invert", "fsync_invert", "fsync_freq", + "mclk_freq", "pm_gate_clocks", "tdm_slots", "tdm_slot_width", + "tx_slots", "rx_slots", "tx_channels", "rx_channels"}, + .string_config_ids = {"format", "bclk", "fsync", "mclk"}, +}; + +const struct config_template_items be_dai_config = { + .int_config_ids = {"id", "default_hw_conf_id", "symmertic_rates", "symmetric_channels", + "symmetric_sample_bits"}, + .string_config_ids = {"stream_name"}, +}; + +const struct config_template_items pcm_config = { + .int_config_ids = {"id", "compress", "symmertic_rates", "symmetric_channels", + "symmetric_sample_bits"}, +}; + +const struct config_template_items mixer_control_config = { + .int_config_ids = {"index", "max", "invert"}, + .compound_config_ids = {"access"} +}; + +const struct config_template_items bytes_control_config = { + .int_config_ids = {"index", "base", "num_regs", "max", "mask"}, + .compound_config_ids = {"access"} +}; + +const struct config_template_items enum_control_config = { + .int_config_ids = {"index"}, + .compound_config_ids = {"access"} +}; + +const struct config_template_items text_config = { + .compound_config_ids = {"values"} +}; + +const struct config_template_items scale_config = { + .int_config_ids = {"min", "step", "mute"}, +}; + +const struct config_template_items ops_config = { + .int_config_ids = {"get", "put"}, + .string_config_ids = {"info"}, +}; + +const struct config_template_items channel_config = { + .int_config_ids = {"reg", "shift"}, +}; + +const struct config_template_items widget_config = { + .int_config_ids = {"index", "no_pm", "shift", "invert", "subseq", "event_type", + "event_flags"}, + .string_config_ids = {"type", "stream_name"}, +}; + +const struct config_template_items data_config = { + .string_config_ids = {"bytes"} +}; + +/* + * Items without class name should be placed lower than those with one, + * because they are much more generic. + */ +const struct build_function_map object_build_map[] = { + {"Base", "manifest", "SectionManifest", &tplg_build_generic_object, NULL, NULL}, + {"Base", "data", "SectionData", &tplg_build_data_object, NULL, &data_config}, + {"Base", "tlv", "SectionTLV", &tplg_build_tlv_object, NULL, NULL}, + {"Base", "scale", "scale", &tplg_build_scale_object, NULL, &scale_config}, + {"Base", "ops", "ops" ,&tplg_build_ops_object, NULL, &ops_config}, + {"Base", "extops", "extops" ,&tplg_build_ops_object, NULL, &ops_config}, + {"Base", "channel", "channel", &tplg_build_channel_object, NULL, &channel_config}, + {"Base", "text", "SectionText", &tplg_build_text_object, NULL, &text_config}, + {"Base", "VendorToken", "SectionVendorTokens", &tplg_build_vendor_token_object, + NULL, NULL}, + {"Base", "hw_config", "SectionHWConfig", &tplg_build_hw_cfg_object, NULL, + &hwcfg_config}, + {"Base", "fe_dai", "dai", &tplg_build_fe_dai_object, NULL, &fe_dai_config}, + {"Base", "route", "SectionGraph", &tplg_build_dapm_route_object, NULL, NULL}, + {"Widget", "buffer", "SectionWidget", &tplg_build_generic_object, NULL, &widget_config}, + {"Widget", "", "SectionWidget", &tplg_build_generic_object, NULL, &widget_config}, + {"Control", "mixer", "SectionControlMixer", &tplg_build_mixer_control, NULL, + &mixer_control_config}, + {"Control", "bytes", "SectionControlBytes", &tplg_build_bytes_control, NULL, + &bytes_control_config}, + {"Control", "enum", "SectionControlEnum", &tplg_build_enum_control, NULL, + &enum_control_config}, + {"Dai", "", "SectionBE", &tplg_build_generic_object, NULL, &be_dai_config}, + {"PCM", "pcm", "SectionPCM", &tplg_build_generic_object, NULL, &pcm_config}, + {"PCM", "pcm_caps", "SectionPCMCapabilities", &tplg_build_pcm_caps_object, + NULL, &pcm_caps_config}, +}; + +static const struct build_function_map *tplg_object_get_map(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *obj) +{ + snd_config_iterator_t first; + snd_config_t *class; + const char *class_type, *class_name; + unsigned int i; + + first = snd_config_iterator_first(obj); + class = snd_config_iterator_entry(first); + + if (snd_config_get_id(class, &class_name) < 0) + return NULL; + + if (snd_config_get_id(obj, &class_type) < 0) + return NULL; + + for (i = 0; i < ARRAY_SIZE(object_build_map); i++) { + if (!strcmp(class_type, "Widget") && + !strcmp(object_build_map[i].class_type, "Widget") && + !strcmp(object_build_map[i].class_name, "")) + return &object_build_map[i]; + + if (!strcmp(class_type, "Dai") && + !strcmp(object_build_map[i].class_type, "Dai")) + return &object_build_map[i]; + + /* for other type objects, also match the object class_name */ + if (!strcmp(class_type, object_build_map[i].class_type) && + !strcmp(object_build_map[i].class_name, class_name)) + return &object_build_map[i]; + } + + return NULL; +} + +/* search for section name based on class type and name and return the config in output_cfg */ +snd_config_t *tplg_object_get_section(struct tplg_pre_processor *tplg_pp, snd_config_t *class) +{ + const struct build_function_map *map; + snd_config_t *cfg = NULL; + int ret; + + map = tplg_object_get_map(tplg_pp, class); + if (!map) + return NULL; + + ret = snd_config_search(tplg_pp->output_cfg, map->section_name, &cfg); + if (ret < 0) + SNDERR("Section config for %s not found\n", map->section_name); + + return cfg; +} + +/* return 1 if attribute not found in search_config, 0 on success and negative value on error */ +static int tplg_object_copy_and_add_param(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *obj, + snd_config_t *attr_cfg, + snd_config_t *search_config) +{ + snd_config_iterator_t first = snd_config_iterator_first(obj); + snd_config_t *attr, *new, *first_cfg; + const char *id, *search_id; + int ret; + + first_cfg = snd_config_iterator_entry(first); + + if (snd_config_get_id(attr_cfg, &id) < 0) + return 0; + + if (snd_config_get_id(search_config, &search_id) < 0) + return 0; + + /* copy object value */ + ret = snd_config_search(search_config, id, &attr); + if (ret < 0) + return 1; + + ret = snd_config_copy(&new, attr); + if (ret < 0) { + SNDERR("error copying attribute '%s' value from %s\n", id, search_id); + return ret; + } + + if (first_cfg) { + /* prepend the new config */ + ret = snd_config_add_before(first_cfg, new); + if (ret < 0) { + snd_config_delete(new); + SNDERR("error prepending attribute '%s' value to %s\n", id, search_id); + } + } else { + ret = snd_config_add(obj, new); + if (ret < 0) { + snd_config_delete(new); + SNDERR("error adding attribute '%s' value to %s\n", id, search_id); + } + } + + return ret; +} + +/* + * Attribute values for an object can be set in one of the following in order of + * precedence: + * 1. Value set in object instance + * 2. Default value set in the object's class definition + * 3. Inherited value from the parent object + * 4. Value set in the object instance embedded in the parent object + * 5. Value set in the object instance embedded in the parent class definition + */ +static int tplg_object_update(struct tplg_pre_processor *tplg_pp, snd_config_t *obj, + snd_config_t *parent) +{ + snd_config_iterator_t i, next; + snd_config_t *n, *cfg, *args; + snd_config_t *obj_cfg, *class_cfg, *parent_obj; + const char *obj_id, *class_name, *class_type; + int ret; + + class_cfg = tplg_class_lookup(tplg_pp, obj); + if (!class_cfg) + return -EINVAL; + + /* find config for class attributes */ + ret = snd_config_search(class_cfg, "DefineAttribute", &args); + if (ret < 0) + return 0; + + if (snd_config_get_id(obj, &class_type) < 0) + return 0; + + if (snd_config_get_id(class_cfg, &class_name) < 0) + return 0; + + /* get obj cfg */ + obj_cfg = tplg_object_get_instance_config(tplg_pp, obj); + if (snd_config_get_id(obj_cfg, &obj_id) < 0) + return 0; + + /* copy and add attributes */ + snd_config_for_each(i, next, args) { + snd_config_t *attr; + const char *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (tplg_class_is_attribute_unique(id, class_cfg)) + continue; + + if (tplg_class_is_attribute_immutable(id, class_cfg)) + goto class; + + /* check if attribute value is set in the object */ + ret = snd_config_search(obj_cfg, id, &attr); + if (ret < 0) + goto class; + continue; +class: + /* search for attributes value in class */ + ret = tplg_object_copy_and_add_param(tplg_pp, obj_cfg, n, class_cfg); + if (ret == 1) { + if (tplg_class_is_attribute_immutable(id, class_cfg)) { + SNDERR("Immutable attribute %s not set in class %s\n", + id, class_name); + return -EINVAL; + } + goto parent; + } + else if (ret < 0) + return ret; + continue; +parent: + /* search for attribute value in parent */ + if (!parent) + goto parent_object; + + /* get parent obj cfg */ + parent_obj = tplg_object_get_instance_config(tplg_pp, parent); + if (!parent_obj) + goto parent_object; + + ret = tplg_object_copy_and_add_param(tplg_pp, obj_cfg, n, parent_obj); + if (ret == 1) + goto parent_object; + else if (ret < 0) + return ret; + continue; +parent_object: + if (!parent) + goto parent_class; + + cfg = tplg_object_lookup_in_config(tplg_pp, parent_obj, class_type, + class_name, obj_id); + if (!cfg) + goto parent_class; + + ret = tplg_object_copy_and_add_param(tplg_pp, obj_cfg, n, cfg); + if (ret == 1) + goto parent_class; + else if (ret < 0) + return ret; + continue; +parent_class: + if (!parent) + goto check; + + cfg = tplg_class_lookup(tplg_pp, parent); + if (!cfg) + return -EINVAL; + + cfg = tplg_object_lookup_in_config(tplg_pp, cfg, class_type, + class_name, obj_id); + if (!cfg) + goto check; + + ret = tplg_object_copy_and_add_param(tplg_pp, obj_cfg, n, cfg); + if (ret == 1) + goto check; + else if (ret < 0) + return ret; + continue; +check: + if (tplg_class_is_attribute_mandatory(id, class_cfg)) { + SNDERR("Mandatory attribute %s not set for class %s\n", id, class_name); + return -EINVAL; + } + } + + return 0; +} + +static int tplg_object_pre_process_children(struct tplg_pre_processor *tplg_pp, + snd_config_t *parent, snd_config_t *cfg) +{ + snd_config_iterator_t i, next; + snd_config_t *children, *n; + int ret; + + ret = snd_config_search(cfg, "Object", &children); + if (ret < 0) + return 0; + + /* create all embedded objects */ + snd_config_for_each(i, next, children) { + const char *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + ret = tplg_pre_process_objects(tplg_pp, n, parent); + if (ret < 0) + return ret; + } + + return 0; +} + +static int tplg_construct_object_name(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *obj, snd_config_t *class_cfg) +{ + snd_config_iterator_t i, next; + snd_config_t *args, *n; + const char *id, *class_id, *obj_id, *s; + char *new_name; + int ret; + + /* find config for class constructor attributes. Nothing to do if not defined */ + ret = snd_config_search(class_cfg, "attributes.constructor", &args); + if (ret < 0) + return 0; + + /* set class name as the name prefix for the object */ + if (snd_config_get_id(obj, &obj_id) < 0) + return -EINVAL; + if (snd_config_get_id(class_cfg, &class_id) < 0) + return -EINVAL; + new_name = strdup(class_id); + if (!new_name) + return -ENOMEM; + + /* iterate through all class arguments and set object name */ + snd_config_for_each(i, next, args) { + snd_config_t *arg; + char *arg_value, *temp; + + n = snd_config_iterator_entry(i); + + if (snd_config_get_id(n, &id) < 0) { + SNDERR("Invalid ID for constructor argument\n"); + ret = -EINVAL; + goto err; + } + + if (snd_config_get_string(n, &s) < 0) { + SNDERR("Invalid value for constructor argument\n"); + ret = -EINVAL; + goto err; + } + + /* find and replace with value set in object */ + ret = snd_config_search(obj, s, &arg); + if (ret < 0) { + SNDERR("Argument %s not set for object '%s.%s'\n", s, class_id, obj_id); + ret = -ENOENT; + goto err; + } + + /* concat arg value to object name. arg types must be either integer or string */ + switch (snd_config_get_type(arg)) { + case SND_CONFIG_TYPE_INTEGER: + { + long v; + ret = snd_config_get_integer(arg, &v); + assert(ret >= 0); + + arg_value = tplg_snprintf("%ld", v); + if (!arg_value) { + ret = -ENOMEM; + goto err; + } + break; + } + case SND_CONFIG_TYPE_STRING: + { + const char *s; + + ret = snd_config_get_string(arg, &s); + assert(ret >= 0); + + arg_value = strdup(s); + if (!arg_value) { + ret = -ENOMEM; + goto err; + } + break; + } + default: + SNDERR("Argument '%s' in object '%s.%s' is not an integer or a string\n", + s, class_id, obj_id); + ret = -EINVAL; + goto err; + } + + /* alloc and concat arg value to the name */ + temp = tplg_snprintf("%s.%s", new_name, arg_value); + free(arg_value); + if (!temp) { + ret = -ENOMEM; + goto err; + } + free(new_name); + new_name = temp; + } + + ret = snd_config_set_id(obj, new_name); +err: + free(new_name); + return ret; +} + +/* set the attribute value by type */ +static int tplg_set_attribute_value(snd_config_t *attr, const char *value) +{ + int err; + snd_config_type_t type = snd_config_get_type(attr); + + switch (type) { + case SND_CONFIG_TYPE_INTEGER: + { + long v; + + v = strtol(value, NULL, 10); + err = snd_config_set_integer(attr, v); + assert(err >= 0); + break; + } + case SND_CONFIG_TYPE_INTEGER64: + { + long long v; + + v = strtoll(value, NULL, 10); + err = snd_config_set_integer64(attr, v); + assert(err >= 0); + break; + } + case SND_CONFIG_TYPE_STRING: + { + err = snd_config_set_string(attr, value); + assert(err >= 0); + break; + } + default: + return -EINVAL; + } + + return 0; +} + + +/* + * Find the unique attribute in the class definition and set its value and type. + * Only string or integer types are allowed for unique values. + */ +static int tplg_object_set_unique_attribute(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj, snd_config_t *class_cfg, + const char *id) +{ + snd_config_t *unique_attr, *new; + const char *unique_name, *class_id; + int ret; + + if (snd_config_get_id(class_cfg, &class_id) < 0) + return 0; + + /* find config for class unique attribute */ + unique_name = tplg_class_get_unique_attribute_name(tplg_pp, class_cfg); + if (!unique_name) + return -ENOENT; + + /* find the unique attribute definition in the class */ + unique_attr = tplg_class_find_attribute_by_name(tplg_pp, class_cfg, unique_name); + if (!unique_attr) + return -ENOENT; + + /* override value if unique attribute is set in the object instance */ + ret = snd_config_search(obj, unique_name, &new); + if (ret < 0) { + ret = snd_config_make(&new, unique_name, + tplg_class_get_attribute_type(tplg_pp, unique_attr)); + if (ret < 0) { + SNDERR("error creating new attribute cfg for object %s\n", id); + return ret; + } + ret = snd_config_add(obj, new); + if (ret < 0) { + SNDERR("error adding new attribute cfg for object %s\n", id); + return ret; + } + } + + ret = tplg_set_attribute_value(new, id); + if (ret < 0) { + SNDERR("error setting unique attribute cfg for object %s\n", id); + return ret; + } + + return ret; +} + +/* + * Helper function to get object instance config which is 2 nodes down from class_type config. + * ex: Get the pointer to the config node with ID "0" from the input config Widget.pga.0 {} + */ +snd_config_t *tplg_object_get_instance_config(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *class_type) +{ + snd_config_iterator_t first; + snd_config_t *cfg; + + first = snd_config_iterator_first(class_type); + cfg = snd_config_iterator_entry(first); + first = snd_config_iterator_first(cfg); + return snd_config_iterator_entry(first); +} + +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION +static int pre_process_find_variable(snd_config_t **dst, const char *str, snd_config_t *config) +{ + snd_config_iterator_t i, next; + + snd_config_for_each(i, next, config) { + snd_config_t *n; + const char *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (strcmp(id, str)) + continue; + + /* found definition, copy config */ + return snd_config_copy(dst, n); + } + + return -EINVAL; +} + +static int +pre_process_object_variables_expand_fcn(snd_config_t **dst, const char *str, void *private_data) +{ + + struct tplg_pre_processor *tplg_pp = private_data; + snd_config_t *object_cfg = tplg_pp->current_obj_cfg; + snd_config_t *conf_defines; + const char *object_id; + const char *val; + int ret; + + ret = snd_config_search(tplg_pp->input_cfg, "Define", &conf_defines); + if (ret < 0) + return 0; + + /* find variable from global definitions first */ + ret = pre_process_find_variable(dst, str, conf_defines); + if (ret >= 0) + return ret; + + /* No global define found, proceeed to object attribute search */ + if (snd_config_get_id(object_cfg, &object_id) < 0) + return -EINVAL; + + /* find variable from object attribute values if not found in global definitions */ + ret = pre_process_find_variable(dst, str, object_cfg); + if (ret < 0) { + SNDERR("Failed to find definition for attribute %s in '%s' object\n", + str, object_id); + return ret; + } + + /* the extracted value may contain a nested $-expression */ + if (snd_config_get_string(*dst, &val) >= 0) { + if (val[0] == '$') { + char *var = strdup(val); + + snd_config_delete(*dst); + ret = snd_config_evaluate_string(dst, var, + pre_process_object_variables_expand_fcn, + tplg_pp); + free(var); + } + } + + return ret; +} + +/* + * Searches for the first '$VAR_NAME' or '$[]' occurrence in + * *stringp. Allocates memory for it and copies it there. The + * allocated string is returned in '*varname'. If there was a prefix + * before $VAR_NAME, it is returned in '*prefix'. The *stringp is + * moved forward to the char after the $VAR_NAME. + * + * The end of $VAR_NAME is the first char that is not alpha numeric, '_', + * or '\0'. + * + * In '$[]' case all letters but '[' and ']' are allow in + * any sequence. Nested '[]' is also allowed if the number of '[' and + * ']' match. + * + * The function modifies *stringp, and *prefix - if not NULL - points + * to the original *stringp, *varname - if not NULL - is malloced and + * should be freed by the caller. + * + * Returns 0 if the *stringp was an empty string. + * 1 if *prefix or *varname was set + * -ENOMEM if malloc failed + */ +static int tplg_get_varname(char **stringp, char **prefix, char **varname) +{ + size_t prefix_len, varname_len = 0; + + *prefix = NULL; + *varname = NULL; + + prefix_len = strcspn(*stringp, "$"); + *prefix = *stringp; + (*stringp) += prefix_len; + if (**stringp == '$') { + if ((*stringp)[1] == '[') { + int brackets = 1; + varname_len = 1; + + do { + varname_len += strcspn((*stringp) + varname_len + 1, "[]") + 1; + if ((*stringp)[varname_len] == '[') + brackets++; + else if ((*stringp)[varname_len] == ']') + brackets--; + else + break; + } while (brackets > 0); + if (brackets != 0) + return -EINVAL; + varname_len++; + } else { + varname_len = 1; + while (isalnum((*stringp)[varname_len]) || (*stringp)[varname_len] == '_') + varname_len++; + } + } + + if (varname_len == 0 && prefix_len == 0) + return 0; + + if (varname_len) { + *varname = malloc(varname_len + 1); + if (*varname == NULL) + return -ENOMEM; + strncpy(*varname, *stringp, varname_len); + (*varname)[varname_len] = '\0'; + (*stringp) += varname_len; + } + + if (prefix_len) + (*prefix)[prefix_len] = '\0'; + else + *prefix = NULL; + + return 1; +} + +static int tplg_evaluate_config_string(struct tplg_pre_processor *tplg_pp, + snd_config_t **dst, const char *s, const char *id) +{ + char *str = strdup(s); + char *varname, *prefix, *freep = str; + int ret; + + if (!str) + return -ENOMEM; + + *dst = NULL; + + /* split the string and expand global definitions or object attribute values */ + while (tplg_get_varname(&str, &prefix, &varname) == 1) { + const char *current_str; + char *temp; + + if (prefix) { + if (*dst == NULL) { + ret = snd_config_make(dst, id, SND_CONFIG_TYPE_STRING); + if (ret < 0) + goto out; + ret = snd_config_set_string(*dst, prefix); + if (ret < 0) + goto out; + } else { + /* concat the prefix */ + snd_config_get_string(*dst, ¤t_str); + temp = tplg_snprintf("%s%s", current_str, prefix); + if (!temp) { + ret = -ENOMEM; + goto out; + } + + ret = snd_config_set_string(*dst, temp); + free(temp); + if (ret < 0) + goto out; + } + } + + if (varname) { + snd_config_t *tmp_config; + + ret = snd_config_evaluate_string(&tmp_config, varname, + pre_process_object_variables_expand_fcn, + tplg_pp); + if (ret < 0) + goto out; + + if (*dst == NULL) { + *dst = tmp_config; + } else { + char *ascii; + + snd_config_get_string(*dst, ¤t_str); + + ret = snd_config_get_ascii(tmp_config, &ascii); + if (ret) + goto out; + + temp = tplg_snprintf("%s%s", current_str, ascii); + free(ascii); + + if (!temp) { + ret = -ENOMEM; + goto out; + } + + ret = snd_config_set_string(*dst, temp); + free(temp); + snd_config_delete(tmp_config); + if (ret < 0) + goto out; + } + free(varname); + } + } + + free(freep); + snd_config_set_id(*dst, id); + + return 0; +out: + if (*dst) + snd_config_delete(*dst); + free(varname); + free(freep); + return ret; +} + +#endif + +/* build object config and its child objects recursively */ +static int tplg_build_object(struct tplg_pre_processor *tplg_pp, snd_config_t *new_obj, + snd_config_t *parent) +{ + snd_config_t *obj_local, *class_cfg; + const struct build_function_map *map; + snd_config_iterator_t i, next; + build_func builder; + update_auto_attr_func auto_attr_updater; + const char *id, *class_id; + int ret; + + obj_local = tplg_object_get_instance_config(tplg_pp, new_obj); + if (!obj_local) + return -EINVAL; + + class_cfg = tplg_class_lookup(tplg_pp, new_obj); + if (!class_cfg) + return -EINVAL; + + if (snd_config_get_id(obj_local, &id) < 0) + return 0; + + if (snd_config_get_id(class_cfg, &class_id) < 0) + return 0; + + /* set unique attribute value */ + ret = tplg_object_set_unique_attribute(tplg_pp, obj_local, class_cfg, id); + if (ret < 0) { + SNDERR("error setting unique attribute value for '%s.%s'\n", class_id, id); + return ret; + } + + /* update object attributes and validate them */ + ret = tplg_object_update(tplg_pp, new_obj, parent); + if (ret < 0) { + SNDERR("Failed to update attributes for object '%s.%s'\n", class_id, id); + return ret; + } + +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + tplg_pp_config_debug(tplg_pp, obj_local); + + /* expand all non-compound type child configs in object */ + snd_config_for_each(i, next, obj_local) { + snd_config_t *n, *new, *class_attr; + const char *id, *s; + char *attr_config_name; + + n = snd_config_iterator_entry(i); + + if (snd_config_get_type(n) == SND_CONFIG_TYPE_COMPOUND) + continue; + + if (snd_config_get_id(n, &id) < 0) + continue; + + if (snd_config_get_string(n, &s) < 0) + continue; + + if (!strstr(s, "$")) + goto validate; + + tplg_pp->current_obj_cfg = obj_local; + + /* Expand definitions and object attribute references. */ + ret = tplg_evaluate_config_string(tplg_pp, &new, s, id); + if (ret < 0) { + SNDERR("Failed to evaluate attributes %s in %s, from '%s'\n", + id, class_id, s); + return ret; + } + + ret = snd_config_merge(n, new, true); + if (ret < 0) + return ret; +validate: + /* validate attribute value */ + snd_config_get_id(n, &id); + attr_config_name = tplg_snprintf("DefineAttribute.%s", id); + if (!attr_config_name) + return -ENOMEM; + + ret = snd_config_search(class_cfg, attr_config_name, &class_attr); + free(attr_config_name); + if (ret < 0) + continue; + + if (!tplg_object_is_attribute_valid(tplg_pp, class_attr, n)) { + SNDERR("Failed to validate attribute %s in %s\n", id, class_id); + return -EINVAL; + } + } +#endif + + /* construct object name using class constructor */ + ret = tplg_construct_object_name(tplg_pp, obj_local, class_cfg); + if (ret < 0) { + SNDERR("Failed to construct object name for %s\n", id); + return ret; + } + + /* + * Build objects if object type is supported. + * If not, process object attributes and add to parent's data section + */ + map = tplg_object_get_map(tplg_pp, new_obj); + if (map) { + builder = map->builder; + + /* update automatic attribute for current object */ + auto_attr_updater = map->auto_attr_updater; + if(auto_attr_updater) { + ret = auto_attr_updater(tplg_pp, obj_local, parent); + if (ret < 0) { + SNDERR("Failed to update automatic attributes for %s\n", id); + return ret; + } + } + } else { + builder = &tplg_build_parent_data; + } + + ret = builder(tplg_pp, new_obj, parent); + if (ret < 0) + return ret; + + /* create child objects in the object instance */ + ret = tplg_object_pre_process_children(tplg_pp, new_obj, obj_local); + if (ret < 0) { + SNDERR("error processing child objects in object %s\n", id); + return ret; + } + + /* create child objects in the object's class definition */ + ret = tplg_object_pre_process_children(tplg_pp, new_obj, class_cfg); + if (ret < 0) + SNDERR("error processing child objects in class %s\n", class_id); + + return ret; +} + +/* create top-level topology objects */ +int tplg_pre_process_objects(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg, + snd_config_t *parent) +{ + snd_config_iterator_t i, next, i2, next2; + snd_config_t *n, *n2, *_obj_type, *_obj_class, *_obj; + const char *id, *class_type, *class_name; + int ret; + + if (snd_config_get_id(cfg, &class_type) < 0) + return 0; + + /* create all objects of the same type and class */ + snd_config_for_each(i, next, cfg) { + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &class_name) < 0) + continue; + snd_config_for_each(i2, next2, n) { + snd_config_t *temp_n2; + + n2 = snd_config_iterator_entry(i2); + if (snd_config_get_id(n2, &id) < 0) { + SNDERR("Invalid id for object\n"); + return -EINVAL; + } + + ret = snd_config_copy(&temp_n2, n2); + if (ret < 0) + return ret; + + /* + * An object declared within a class definition as follows: + * Class.Pipeline.volume-playback { + * Object.Widget.pga.0 { + * ramp_step_ms 250 + * } + * } + * + * While instantiating the volume-pipeline class, the pga object + * could be modified as follows: + * Object.Pipeline.volume-playback.0 { + * Object.Widget.pga.0 { + * format "s24le" + * } + * } + * When building the pga.0 object in the class definition, merge + * the attributes declared in the volume-playback.0 object to create + * a new config as follows to make sure that all attributes are + * set for the pga object. + * Object.Widget.pga.0 { + * ramp_step_ms 250 + * format "s24le" + * } + */ + + if (parent) { + snd_config_t *parent_instance, *parent_obj, *temp; + char *obj_cfg_name; + + obj_cfg_name = tplg_snprintf("%s%s.%s.%s", "Object.", + class_type, class_name, id); + + /* search for object instance in the parent */ + parent_instance = tplg_object_get_instance_config(tplg_pp, parent); + if (!parent_instance) + goto temp_cfg; + + ret = snd_config_search(parent_instance, obj_cfg_name, &parent_obj); + free(obj_cfg_name); + if (ret < 0) + goto temp_cfg; + + /* don't merge if the object configs are the same */ + if (parent_obj == n2) + goto temp_cfg; + + /* create a temp config copying the parent object config */ + ret = snd_config_copy(&temp, parent_obj); + if (ret < 0) { + snd_config_delete(temp_n2); + return ret; + } + + /* + * Merge parent object with the current object instance. + * temp will be deleted by merge + */ + ret = snd_config_merge(temp_n2, temp, false); + if (ret < 0) { + SNDERR("error merging parent object config for %s.%s.%s\n", + class_type, class_name, id); + snd_config_delete(temp_n2); + return ret; + } + } +temp_cfg: + /* create a temp config for object with class type as the root node */ + ret = snd_config_make(&_obj_type, class_type, SND_CONFIG_TYPE_COMPOUND); + if (ret < 0) { + snd_config_delete(temp_n2); + return ret; + } + + ret = snd_config_make(&_obj_class, class_name, SND_CONFIG_TYPE_COMPOUND); + if (ret < 0) + goto err; + + ret = snd_config_add(_obj_type, _obj_class); + if (ret < 0) { + snd_config_delete(_obj_class); + goto err; + } + + ret = snd_config_copy(&_obj, temp_n2); + if (ret < 0) + goto err; + + ret = snd_config_add(_obj_class, _obj); + if (ret < 0) { + snd_config_delete(_obj); + goto err; + } + + /* Build the object now */ + ret = tplg_build_object(tplg_pp, _obj_type, parent); + if (ret < 0) + SNDERR("Error building object %s.%s.%s\n", + class_type, class_name, id); +err: + snd_config_delete(temp_n2); + snd_config_delete(_obj_type); + if (ret < 0) + return ret; + } + } + + return 0; +} diff --git a/topology/pre-processor.c b/topology/pre-processor.c new file mode 100644 index 0000000..95e2a89 --- /dev/null +++ b/topology/pre-processor.c @@ -0,0 +1,1179 @@ +/* + Copyright(c) 2021 Intel Corporation + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + The full GNU General Public License is included in this distribution + in the file called LICENSE.GPL. +*/ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "gettext.h" +#include "topology.h" +#include "pre-processor.h" +#include "pre-process-external.h" + +#define SND_TOPOLOGY_MAX_PLUGINS 32 + +static int get_plugin_string(struct tplg_pre_processor *tplg_pp, char **plugin_string) +{ + const char *lib_names_t = NULL; + snd_config_t *defines; + int ret; + + ret = snd_config_search(tplg_pp->input_cfg, "Define.PREPROCESS_PLUGINS", &defines); + if (ret < 0) + return ret; + + ret = snd_config_get_string(defines, &lib_names_t); + if (ret < 0) + return ret; + + *plugin_string = strdup(lib_names_t); + + if (!*plugin_string) + return -ENOMEM; + + return 0; +} + +static int run_plugin(struct tplg_pre_processor *tplg_pp, char *plugin) +{ + plugin_pre_process process; + char *xlib, *xfunc, *path; + void *h = NULL; + int ret = 0; + + /* compose the plugin path, if not from environment, then from default plugins dir */ + path = getenv("ALSA_TOPOLOGY_PLUGIN_DIR"); + if (!path) + path = ALSA_TOPOLOGY_PLUGIN_DIR; + + xlib = tplg_snprintf("%s/%s%s%s", path, PROCESS_LIB_PREFIX, plugin, + PROCESS_LIB_POSTFIX); + xfunc = tplg_snprintf("%s%s%s", PROCESS_FUNC_PREFIX, plugin, + PROCESS_FUNC_POSTFIX); + + if (!xlib || !xfunc) { + fprintf(stderr, "can't reserve memory for plugin paths and func names\n"); + ret = -ENOMEM; + goto err; + } + + /* open plugin */ + h = dlopen(xlib, RTLD_NOW); + if (!h) { + fprintf(stderr, "unable to open library '%s'\n", xlib); + ret = -EINVAL; + goto err; + } + + /* find function */ + process = dlsym(h, xfunc); + + if (!process) { + fprintf(stderr, "symbol 'topology_process' was not found in %s\n", xlib); + ret = -EINVAL; + goto err; + } + + /* process plugin */ + ret = process(tplg_pp->input_cfg, tplg_pp->output_cfg); + +err: + if (h) + dlclose(h); + if (xlib) + free(xlib); + if (xfunc) + free(xfunc); + + return ret; +} + +static int pre_process_plugins(struct tplg_pre_processor *tplg_pp) +{ + char *plugins[SND_TOPOLOGY_MAX_PLUGINS]; + char *plugin_string; + int count; + int ret; + int i; + + /* parse plugin names */ + ret = get_plugin_string(tplg_pp, &plugin_string); + + /* no plugins defined, so just return */ + if (ret < 0) + return 0; + + count = 0; + plugins[count] = strtok(plugin_string, ":"); + while ((count < SND_TOPOLOGY_MAX_PLUGINS - 1) && plugins[count]) { + count++; + plugins[count] = strtok(NULL, ":"); + } + + /* run all plugins */ + for (i = 0; i < count; i++) { + ret = run_plugin(tplg_pp, plugins[i]); + if (ret < 0) + return ret; + } + + free(plugin_string); + + return 0; +} + +/* + * Helper function to find config by id. + * Topology2.0 object names are constructed with attribute values separated by '.'. + * So snd_config_search() cannot be used as it interprets the '.' as the node separator. + */ +snd_config_t *tplg_find_config(snd_config_t *config, const char *name) +{ + snd_config_iterator_t i, next; + snd_config_t *n; + const char *id; + + snd_config_for_each(i, next, config) { + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (!strcmp(id, name)) + return n; + } + + return NULL; +} + +/* make a new config and add it to parent */ +int tplg_config_make_add(snd_config_t **config, const char *id, snd_config_type_t type, + snd_config_t *parent) +{ + int ret; + + ret = snd_config_make(config, id, type); + if (ret < 0) + return ret; + + ret = snd_config_add(parent, *config); + if (ret < 0) + snd_config_delete(*config); + + return ret; +} + +/* + * The pre-processor will need to concat multiple strings separate by '.' to construct the object + * name and search for configs with ID's separated by '.'. + * This function helps concat input strings in the specified input format + */ +char *tplg_snprintf(char *fmt, ...) +{ + char *string; + int len = 1; + + va_list va; + + va_start(va, fmt); + len += vsnprintf(NULL, 0, fmt, va); + va_end(va); + + string = calloc(1, len); + if (!string) + return NULL; + + va_start(va, fmt); + vsnprintf(string, len, fmt, va); + va_end(va); + + return string; +} + +#ifdef TPLG_DEBUG +void tplg_pp_debug(char *fmt, ...) +{ + char msg[DEBUG_MAX_LENGTH]; + va_list va; + + va_start(va, fmt); + vsnprintf(msg, DEBUG_MAX_LENGTH, fmt, va); + va_end(va); + + fprintf(stdout, "%s\n", msg); +} + +void tplg_pp_config_debug(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg) +{ + snd_config_save(cfg, tplg_pp->dbg_output); +} +#else +void tplg_pp_debug(char *fmt ATTRIBUTE_UNUSED, ...) {} +void tplg_pp_config_debug(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + snd_config_t *cfg ATTRIBUTE_UNUSED) {} +#endif + +static int pre_process_config(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg) +{ + snd_config_iterator_t i, next, i2, next2; + snd_config_t *n, *n2; + const char *id; + int err; + + if (snd_config_get_type(cfg) != SND_CONFIG_TYPE_COMPOUND) { + fprintf(stderr, "compound type expected at top level"); + return -EINVAL; + } + + /* parse topology objects */ + snd_config_for_each(i, next, cfg) { + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + if (strcmp(id, "Object")) + continue; + + if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND) { + fprintf(stderr, "compound type expected for %s", id); + return -EINVAL; + } + + snd_config_for_each(i2, next2, n) { + n2 = snd_config_iterator_entry(i2); + + if (snd_config_get_id(n2, &id) < 0) + continue; + + if (snd_config_get_type(n2) != SND_CONFIG_TYPE_COMPOUND) { + fprintf(stderr, "compound type expected for %s", id); + return -EINVAL; + } + + /* pre-process Object instance. Top-level object have no parent */ + err = tplg_pre_process_objects(tplg_pp, n2, NULL); + if (err < 0) + return err; + } + } + + return 0; +} + +void free_pre_processor(struct tplg_pre_processor *tplg_pp) +{ + snd_output_close(tplg_pp->output); + snd_output_close(tplg_pp->dbg_output); + snd_config_delete(tplg_pp->output_cfg); + if (tplg_pp->define_cfg) + snd_config_delete(tplg_pp->define_cfg); + free(tplg_pp->inc_path); + free(tplg_pp); +} + +int init_pre_processor(struct tplg_pre_processor **tplg_pp, snd_output_type_t type, + const char *output_file) +{ + struct tplg_pre_processor *_tplg_pp; + int ret; + + _tplg_pp = calloc(1, sizeof(struct tplg_pre_processor)); + if (!_tplg_pp) + return -ENOMEM; + + *tplg_pp = _tplg_pp; + + /* create output top-level config node */ + ret = snd_config_top(&_tplg_pp->output_cfg); + if (ret < 0) + goto err; + + /* open output based on type */ + if (type == SND_OUTPUT_STDIO) { + ret = snd_output_stdio_open(&_tplg_pp->output, output_file, "w"); + if (ret < 0) { + fprintf(stderr, "failed to open file output\n"); + goto open_err; + } + } else { + ret = snd_output_buffer_open(&_tplg_pp->output); + if (ret < 0) { + fprintf(stderr, "failed to open buffer output\n"); + goto open_err; + } + } + + /* debug output */ + ret = snd_output_stdio_attach(&_tplg_pp->dbg_output, stdout, 0); + if (ret < 0) { + fprintf(stderr, "failed to open stdout output\n"); + goto out_close; + } + + return 0; +out_close: + snd_output_close(_tplg_pp->output); +open_err: + snd_config_delete(_tplg_pp->output_cfg); +err: + free(_tplg_pp); + return ret; +} + +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION +static int pre_process_set_defines(struct tplg_pre_processor *tplg_pp, const char *pre_processor_defs) +{ + int ret; + + /* + * load the command line defines to the configuration tree + */ + if (pre_processor_defs != NULL) { + ret = snd_config_load_string(&tplg_pp->define_cfg, pre_processor_defs, 0); + if (ret < 0) { + fprintf(stderr, "Failed to load pre-processor command line definitions\n"); + return ret; + } + } else { + tplg_pp->define_cfg = NULL; + } + + return 0; +} + +static int pre_process_add_defines(struct tplg_pre_processor *tplg_pp, snd_config_t *from) +{ + snd_config_t *conf_defines, *conf_tmp; + int ret; + + ret = snd_config_search(from, "Define", &conf_defines); + if (ret == -ENOENT) { + if (tplg_pp->input_cfg == from && tplg_pp->define_cfg) { + conf_defines = NULL; + goto create; + } + } + if (ret < 0) + return ret; + + if (snd_config_get_type(conf_defines) != SND_CONFIG_TYPE_COMPOUND) { + fprintf(stderr, "Define must be a compound!\n"); + return -EINVAL; + } + + if (tplg_pp->input_cfg == from) + tplg_pp->define_cfg_merged = conf_defines; + + if (tplg_pp->define_cfg_merged == NULL) { +create: + ret = snd_config_make_compound(&tplg_pp->define_cfg_merged, "Define", 0); + if (ret < 0) + return ret; + ret = snd_config_add(tplg_pp->input_cfg, tplg_pp->define_cfg_merged); + if (ret < 0) + return ret; + } + + if (tplg_pp->define_cfg_merged != conf_defines) { + /* + * merge back to the main configuration tree (Define subtree) + */ + ret = snd_config_merge(tplg_pp->define_cfg_merged, conf_defines, true); + if (ret < 0) { + fprintf(stderr, "Failed to override main variable definitions\n"); + return ret; + } + } + + /* + * merge the command line defines with the variables in the conf file to override + * default values; use a copy (merge deletes the source tree) + */ + if (tplg_pp->define_cfg) { + ret = snd_config_copy(&conf_tmp, tplg_pp->define_cfg); + if (ret < 0) { + fprintf(stderr, "Failed to copy variable definitions\n"); + return ret; + } + ret = snd_config_merge(tplg_pp->define_cfg_merged, conf_tmp, true); + if (ret < 0) { + fprintf(stderr, "Failed to override variable definitions\n"); + snd_config_delete(conf_tmp); + return ret; + } + } + + return 0; +} + +static int pre_process_includes(struct tplg_pre_processor *tplg_pp, snd_config_t *top); + +static int pre_process_include_conf(struct tplg_pre_processor *tplg_pp, snd_config_t *config, + snd_config_t **new, snd_config_t *variable) +{ + snd_config_iterator_t i, next; + const char *variable_name; + char *value; + int ret; + + if (snd_config_get_id(variable, &variable_name) < 0) + return 0; + + switch(snd_config_get_type(variable)) { + case SND_CONFIG_TYPE_STRING: + { + const char *s; + + if (snd_config_get_string(variable, &s) < 0) { + SNDERR("Invalid value for variable %s\n", variable_name); + return -EINVAL; + } + value = strdup(s); + if (!value) + return -ENOMEM; + break; + } + case SND_CONFIG_TYPE_INTEGER: + { + long v; + + ret = snd_config_get_integer(variable, &v); + if (ret < 0) { + SNDERR("Invalid value for variable %s\n", variable_name); + return ret; + } + + value = tplg_snprintf("%ld", v); + if (!value) + return -ENOMEM; + break; + } + default: + SNDERR("Invalid type for variable definition %s\n", variable_name); + return -EINVAL; + } + + /* create top-level config node */ + ret = snd_config_top(new); + if (ret < 0) { + SNDERR("failed to create top-level node for include conf %s\n", variable_name); + goto err; + } + + snd_config_for_each(i, next, config) { + snd_input_t *in; + snd_config_t *n; + regex_t regex; + const char *filename; + const char *id; + char *full_path; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + ret = regcomp(®ex, id, REG_EXTENDED | REG_ICASE); + if (ret) { + fprintf(stderr, "Could not compile regex\n"); + goto err; + } + + /* Execute regular expression */ + ret = regexec(®ex, value, 0, NULL, 0); + if (ret) + continue; + + /* regex matched. now include or use the configuration */ + if (snd_config_get_type(n) == SND_CONFIG_TYPE_COMPOUND) { + /* configuration block */ + ret = snd_config_merge(*new, n, 0); + if (ret < 0) { + fprintf(stderr, "Unable to merge key '%s'\n", value); + goto err; + } + } else { + ret = snd_config_get_string(n, &filename); + if (ret < 0) + goto err; + + if (filename && filename[0] != '/') + full_path = tplg_snprintf("%s/%s", tplg_pp->inc_path, filename); + else + full_path = tplg_snprintf("%s", filename); + + ret = snd_input_stdio_open(&in, full_path, "r"); + if (ret < 0) { + fprintf(stderr, "Unable to open included conf file %s\n", full_path); + free(full_path); + goto err; + } + free(full_path); + + /* load config */ + ret = snd_config_load(*new, in); + snd_input_close(in); + if (ret < 0) { + fprintf(stderr, "Unable to load included configuration\n"); + goto err; + } + } + + /* forcefully overwrite with defines from the command line */ + ret = pre_process_add_defines(tplg_pp, *new); + if (ret < 0 && ret != -ENOENT) { + fprintf(stderr, "Failed to parse arguments in input config\n"); + goto err; + } + + /* recursively process any nested includes */ + ret = pre_process_includes(tplg_pp, *new); + if (ret < 0) + goto err; + } + +err: + free(value); + return ret; +} + +static int pre_process_includes(struct tplg_pre_processor *tplg_pp, snd_config_t *top) +{ + snd_config_iterator_t i, next; + snd_config_t *includes; + const char *top_id; + int ret; + + if (tplg_pp->define_cfg_merged == NULL) + return 0; + + ret = snd_config_search(top, "IncludeByKey", &includes); + if (ret < 0) + return 0; + + snd_config_get_id(top, &top_id); + + snd_config_for_each(i, next, includes) { + snd_config_t *n, *new, *define; + const char *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + /* find id from variable definitions */ + ret = snd_config_search(tplg_pp->define_cfg_merged, id, &define); + if (ret < 0) { + fprintf(stderr, "No variable defined for %s\n", id); + return ret; + } + + /* create conf node from included file */ + ret = pre_process_include_conf(tplg_pp, n, &new, define); + if (ret < 0) { + fprintf(stderr, "Unable to process include file \n"); + return ret; + } + + /* merge the included conf file with the top-level conf */ + ret = snd_config_merge(top, new, 0); + if (ret < 0) { + fprintf(stderr, "Failed to add included conf\n"); + return ret; + } + } + + /* delete all includes from current top */ + snd_config_delete(includes); + + return 0; +} + +static int pre_process_includes_all(struct tplg_pre_processor *tplg_pp, snd_config_t *top) +{ + snd_config_iterator_t i, next; + int ret; + + if (snd_config_get_type(top) != SND_CONFIG_TYPE_COMPOUND) + return 0; + + /* process includes at this node */ + ret = pre_process_includes(tplg_pp, top); + if (ret < 0) { + fprintf(stderr, "Failed to process includes\n"); + return ret; + } + + /* process includes at all child nodes */ + snd_config_for_each(i, next, top) { + snd_config_t *n; + + n = snd_config_iterator_entry(i); + + ret = pre_process_includes_all(tplg_pp, n); + if (ret < 0) + return ret; + } + + return 0; +} + +/* duplicate the existing objects in src into dest and update with new attribute */ +static int pre_process_add_objects(struct tplg_pre_processor *tplg_pp ATTRIBUTE_UNUSED, + int *object_count, snd_config_t *src, + snd_config_t *dest, snd_config_t *attr_cfg) +{ + snd_config_iterator_t i, next; + int ret; + + snd_config_for_each(i, next, src) { + snd_config_t *n, *new, *new_attr; + char* new_id = tplg_snprintf("%d", (*object_count)++); + + n = snd_config_iterator_entry(i); + + /* duplicate the existing object */ + ret = snd_config_copy(&new, n); + if (ret < 0) { + free(new_id); + return ret; + } + + ret = snd_config_set_id(new, new_id); + free(new_id); + if (ret < 0) { + snd_config_delete(new); + return ret; + } + + ret = snd_config_add(dest, new); + if (ret < 0) { + snd_config_delete(new); + return ret; + } + + /* and update the new attribute */ + ret = snd_config_copy(&new_attr, attr_cfg); + if (ret < 0) + return ret; + + ret = snd_config_add(new, new_attr); + if (ret < 0) { + snd_config_delete(new_attr); + return ret; + } + } + + return 0; +} + +/* Create config based on the number of items in the array */ +static int pre_process_create_items(struct tplg_pre_processor *tplg_pp, + snd_config_t *cfg, snd_config_t *top, + int *object_count_offset) +{ + snd_config_iterator_t i, next; + snd_config_type_t type; + const char *class_id; + int attr_count = 0; + int object_count = *object_count_offset; + int ret; + + snd_config_get_id(top, &class_id); + + snd_config_for_each(i, next, cfg) { + snd_config_iterator_t i2, next2; + snd_config_t *n, *local_top; + const char *attribute; + int attr_val_count = 0; + object_count = *object_count_offset; + + n = snd_config_iterator_entry(i); + type = snd_config_get_type(n); + if (type != SND_CONFIG_TYPE_COMPOUND) + continue; + if (snd_config_get_id(n, &attribute) < 0) + continue; + + ret = snd_config_make(&local_top, class_id, SND_CONFIG_TYPE_COMPOUND); + + snd_config_for_each(i2, next2, n) { + snd_config_t *n2, *new, *new_obj; + snd_config_type_t attr_type; + char *new_id; + + n2 = snd_config_iterator_entry(i2); + + attr_type = snd_config_get_type(n2); + + /* create new config based on type */ + if (attr_type == SND_CONFIG_TYPE_INTEGER) { + long val; + + ret = snd_config_get_integer(n2, &val); + if (ret < 0) + return ret; + + ret = snd_config_make(&new, attribute, SND_CONFIG_TYPE_INTEGER); + if (ret < 0) + return ret; + + ret = snd_config_set_integer(new, val); + } else { + const char *s; + + ret = snd_config_get_string(n2, &s); + if (ret < 0) + return ret; + ret = snd_config_make(&new, attribute, SND_CONFIG_TYPE_STRING); + if (ret < 0) + return ret; + + ret = snd_config_set_string(new, s); + } + + if (ret < 0) + goto err; + + /* for the first array simply create new conf nodes */ + if (!attr_count) { + new_id = tplg_snprintf("%d", object_count++); + ret = snd_config_make(&new_obj, new_id, SND_CONFIG_TYPE_COMPOUND); + free(new_id); + + ret = snd_config_add(new_obj, new); + if (ret < 0) { + snd_config_delete(new_obj); + goto err; + } + + ret = snd_config_add(local_top, new_obj); + if (ret < 0) { + snd_config_delete(new_obj); + goto err; + } + + continue; + } + + /* + * for the subsequent arrays, duplicate the existing objects + * and update them with the new ones + */ + ret = pre_process_add_objects(tplg_pp, &object_count, top, + local_top, new); + if (ret < 0) { + SNDERR("failed to add objects of type %s\n", class_id); + goto err; + } + + attr_val_count++; +err: + snd_config_delete(new); + if (ret < 0) { + snd_config_delete(local_top); + return ret; + } + } + + /* substitute current list of configs with the updated list */ + ret = snd_config_substitute(top, local_top); + if (ret < 0) { + snd_config_delete(local_top); + return ret; + } + attr_count++; + } + + *object_count_offset = object_count; + return 0; +} + +static int pre_process_array_item(struct tplg_pre_processor *tplg_pp, snd_config_t *top, + snd_config_t *array) +{ + snd_config_iterator_t i, next; + int object_count; + int ret; + + snd_config_for_each(i, next, array) { + snd_config_iterator_t i3, next3; + snd_config_t *n, *new; + const char *id; + + n = snd_config_iterator_entry(i); + if (snd_config_get_id(n, &id) < 0) + continue; + + /* Create a new node if it doesn't exist already */ + if (snd_config_search(top, id, &new) < 0) { + ret = snd_config_make(&new, id, SND_CONFIG_TYPE_COMPOUND); + if (ret < 0) + return ret; + + /* add the list of objects to the current top */ + ret = snd_config_add(top, new); + if (ret < 0) { + snd_config_delete(new); + return ret; + } + } + + /* if the conf node is not an array, move on to parse child nodes */ + if (snd_config_is_array(n) <= 0) + return pre_process_array_item(tplg_pp, new, n); + + object_count = 0; + snd_config_for_each(i3, next3, n) { + snd_config_t *n3, *local_top; + + n3 = snd_config_iterator_entry(i3); + + ret = snd_config_make(&local_top, id, SND_CONFIG_TYPE_COMPOUND); + if (ret < 0) + return ret; + + ret = pre_process_create_items(tplg_pp, n3, local_top, + &object_count); + if (ret < 0) { + SNDERR("failed to create objects of type %s\n", id); + return ret; + } + + ret = snd_config_merge(new, local_top, 0); + if (ret < 0) { + snd_config_delete(local_top); + return ret; + } + } + } + + return 0; +} + +static int pre_process_array(struct tplg_pre_processor *tplg_pp, snd_config_t *top) +{ + snd_config_t *arrays; + int ret; + + ret = snd_config_search(top, "CombineArrays", &arrays); + if (ret < 0) + return 0; + + ret = pre_process_array_item(tplg_pp, top, arrays); + if (ret < 0) + return ret; + + snd_config_delete(arrays); + return 0; +} + +static int pre_process_arrays(struct tplg_pre_processor *tplg_pp, snd_config_t *top) +{ + snd_config_iterator_t i, next; + int ret; + + if (snd_config_get_type(top) != SND_CONFIG_TYPE_COMPOUND) + return 0; + + /* process object arrays at this node */ + ret = pre_process_array(tplg_pp, top); + if (ret < 0) { + fprintf(stderr, "Failed to process object arrays\n"); + return ret; + } + + /* process object arrays at all child nodes */ + snd_config_for_each(i, next, top) { + snd_config_t *n; + + n = snd_config_iterator_entry(i); + + ret = pre_process_arrays(tplg_pp, n); + if (ret < 0) + return ret; + } + + return 0; +} + +static int pre_process_subtree_copy(struct tplg_pre_processor *tplg_pp, snd_config_t *curr, + snd_config_t *top) +{ + snd_config_iterator_t i, next; + snd_config_t *subtrees; + int ret; + + ret = snd_config_search(curr, "SubTreeCopy", &subtrees); + if (ret < 0) + return 0; + + snd_config_for_each(i, next, subtrees) { + snd_config_t *n, *source_cfg, *target_cfg, *type_cfg; + snd_config_t *source_tree, *target_tree, *tmp, *subtree_cfg; + const char *id, *source_id; + const char *type = NULL; + const char *target_id = NULL; + bool override = false; + + n = snd_config_iterator_entry(i); + ret = snd_config_get_id(n, &id); + if (ret < 0) { + SNDERR("Failed to get ID for subtree copy\n"); + return ret; + } + + /* get the type of copy ex: override/extend if set, by default override is false */ + ret = snd_config_search(n, "type", &type_cfg); + if (ret >= 0) { + ret = snd_config_get_string(type_cfg, &type); + if (ret >= 0) { + if (strcmp(type, "override") && strcmp(type, "extend")) { + SNDERR("Invalid value for sub tree copy type %s\n", type); + return ret; + } + + if (!strcmp(type, "override")) + override = true; + } + } + + ret = snd_config_search(n, "source", &source_cfg); + if (ret < 0) { + SNDERR("failed to get source config for subtree %s\n", id); + return ret; + } + + /* if the target node is not set, the subtree will be copied to current node */ + ret = snd_config_search(n, "target", &target_cfg); + if (ret >= 0) { + snd_config_t *temp_target; + char *s; + + ret = snd_config_get_string(target_cfg, &target_id); + if (ret < 0) { + SNDERR("Invalid target id for subtree %s\n", id); + return ret; + } + + /* + * create a temporary node with target_id and merge with top to avoid + * failure in case the target node ID doesn't exist already + */ + s = tplg_snprintf("%s {}", target_id); + if (!s) + return -ENOMEM; + + ret = snd_config_load_string(&temp_target, s, 0); + free(s); + if (ret < 0) { + SNDERR("Cannot create temp node with target id %s\n", target_id); + return ret; + } + + ret = snd_config_merge(top, temp_target, false); + if (ret < 0) { + SNDERR("Cannot merge temp node with target id %s\n", target_id); + return ret; + } + + ret = snd_config_search(top, target_id, &target_tree); + if (ret < 0) { + SNDERR("failed to get target tree %s\n", target_id); + return ret; + } + } else { + target_tree = curr; + } + + /* get the source tree node */ + ret = snd_config_get_string(source_cfg, &source_id); + if (ret < 0) { + SNDERR("Invalid source node id for subtree %s\n", id); + return ret; + } + + ret = snd_config_search(top, source_id, &source_tree); + if (ret < 0) { + SNDERR("failed to get source tree %s\n", source_id); + return ret; + } + + /* get the subtree to be merged */ + ret = snd_config_search(n, "tree", &subtree_cfg); + if (ret < 0) + subtree_cfg = NULL; + + /* make a temp copy of the source tree */ + ret = snd_config_copy(&tmp, source_tree); + if (ret < 0) { + SNDERR("failed to copy source tree for subtreecopy %s\n", id); + return ret; + } + + /* merge the current block with the source tree */ + ret = snd_config_merge(tmp, subtree_cfg, override); + if (ret < 0) { + snd_config_delete(tmp); + SNDERR("Failed to merge source tree w/ subtree %s\n", id); + return ret; + } + + /* merge the merged block to the target tree */ + ret = snd_config_merge(target_tree, tmp, override); + if (ret < 0) { + snd_config_delete(tmp); + SNDERR("Failed to merge subtree %s w/ target\n", id); + return ret; + } + } + + /* all subtree copies have been processed, remove the node */ + snd_config_delete(subtrees); + + return 0; +} + +static int pre_process_subtree_copies(struct tplg_pre_processor *tplg_pp, snd_config_t *top, + snd_config_t *curr) +{ + snd_config_iterator_t i, next; + int ret; + + if (snd_config_get_type(curr) != SND_CONFIG_TYPE_COMPOUND) + return 0; + + /* process subtreecopies at this node */ + ret = pre_process_subtree_copy(tplg_pp, curr, top); + if (ret < 0) + return ret; + + /* process subtreecopies at all child nodes */ + snd_config_for_each(i, next, curr) { + snd_config_t *n; + + n = snd_config_iterator_entry(i); + + /* process subtreecopies at this node */ + ret = pre_process_subtree_copies(tplg_pp, top, n); + if (ret < 0) + return ret; + } + + return 0; +} + +#endif /* version < 1.2.6 */ + +int pre_process(struct tplg_pre_processor *tplg_pp, char *config, size_t config_size, + const char *pre_processor_defs, const char *inc_path) +{ + snd_input_t *in; + snd_config_t *top; + int err; + + /* create input buffer */ + err = snd_input_buffer_open(&in, config, config_size); + if (err < 0) { + fprintf(stderr, "Unable to open input buffer\n"); + return err; + } + + /* create top-level config node */ + err = snd_config_top(&top); + if (err < 0) + goto input_close; + + /* load config */ + err = snd_config_load(top, in); + if (err < 0) { + fprintf(stderr, "Unable not load configuration\n"); + goto err; + } + + tplg_pp->input_cfg = top; + tplg_pp->inc_path = inc_path ? strdup(inc_path) : NULL; + +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + /* parse command line definitions */ + err = pre_process_set_defines(tplg_pp, pre_processor_defs); + if (err < 0) { + fprintf(stderr, "Failed to parse arguments in input config\n"); + goto err; + } + + /* parse command line definitions */ + err = pre_process_add_defines(tplg_pp, top); + if (err < 0) { + fprintf(stderr, "Failed to parse arguments in input config\n"); + goto err; + } + + /* include conditional conf files */ + err = pre_process_includes_all(tplg_pp, tplg_pp->input_cfg); + if (err < 0) { + fprintf(stderr, "Failed to process conditional includes in input config\n"); + goto err; + } + + /* expand object arrays */ + err = pre_process_arrays(tplg_pp, tplg_pp->input_cfg); + if (err < 0) { + fprintf(stderr, "Failed to process object arrays in input config\n"); + goto err; + } + + err = pre_process_subtree_copies(tplg_pp, tplg_pp->input_cfg, tplg_pp->input_cfg); + if (err < 0) { + SNDERR("Failed to process subtree copies in input config\n"); + goto err; + } +#endif + + err = pre_process_config(tplg_pp, tplg_pp->input_cfg); + if (err < 0) { + fprintf(stderr, "Unable to pre-process configuration\n"); + goto err; + } + + /* process topology plugins */ + err = pre_process_plugins(tplg_pp); + if (err < 0) { + fprintf(stderr, "Unable to run pre-process plugins or plugins return error\n"); + goto err; + } + + /* save config to output */ + err = snd_config_save(tplg_pp->output_cfg, tplg_pp->output); + if (err < 0) + fprintf(stderr, "failed to save pre-processed output file\n"); + +err: + snd_config_delete(top); +input_close: + snd_input_close(in); + + return err; +} diff --git a/topology/pre-processor.h b/topology/pre-processor.h new file mode 100644 index 0000000..3a8a4bc --- /dev/null +++ b/topology/pre-processor.h @@ -0,0 +1,123 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __PRE_PROCESSOR_H +#define __PRE_PROCESSOR_H + +#include +#include +#include +#include "topology.h" + +#define DEBUG_MAX_LENGTH 256 +#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a)[0]) + +#define MAX_CONFIGS_IN_TEMPLATE 32 + +struct config_template_items { + char *int_config_ids[MAX_CONFIGS_IN_TEMPLATE]; + char *string_config_ids[MAX_CONFIGS_IN_TEMPLATE]; + char *compound_config_ids[MAX_CONFIGS_IN_TEMPLATE]; +}; + +typedef int (*build_func)(struct tplg_pre_processor *tplg_pp, snd_config_t *obj, + snd_config_t *parent); + +typedef int (*update_auto_attr_func)(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj, snd_config_t *parent); + +struct build_function_map { + char *class_type; + char *class_name; + char *section_name; + build_func builder; + update_auto_attr_func auto_attr_updater; + const struct config_template_items *template_items; +}; + +extern const struct build_function_map object_build_map[]; + +/* debug helpers */ +void tplg_pp_debug(char *fmt, ...); +void tplg_pp_config_debug(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg); + +/* object build helpers */ +int tplg_build_object_from_template(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t **wtop, snd_config_t *top_config, + bool skip_name); +int tplg_build_tlv_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_scale_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_ops_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_channel_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_mixer_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_bytes_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_enum_control(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_text_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_dapm_route_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_hw_cfg_object(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_cfg, snd_config_t *parent); +int tplg_build_fe_dai_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent); +int tplg_build_base_object(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *parent, bool skip_name); +int tplg_build_pcm_caps_object(struct tplg_pre_processor *tplg_pp, + snd_config_t *obj_cfg, snd_config_t *parent); +int tplg_parent_update(struct tplg_pre_processor *tplg_pp, snd_config_t *parent, + const char *section_name, const char *item_name); +int tplg_add_object_data(struct tplg_pre_processor *tplg_pp, snd_config_t *obj_cfg, + snd_config_t *top, const char *array_name); + +/* object helpers */ +int tplg_pre_process_objects(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg, + snd_config_t *parent); +snd_config_t *tplg_object_get_instance_config(struct tplg_pre_processor *tplg_pp, + snd_config_t *class_type); +const char *tplg_object_get_name(struct tplg_pre_processor *tplg_pp, + snd_config_t *object); +snd_config_t *tplg_object_get_section(struct tplg_pre_processor *tplg_pp, snd_config_t *class); + +/* class helpers */ +snd_config_t *tplg_class_lookup(struct tplg_pre_processor *tplg_pp, snd_config_t *cfg); +snd_config_t *tplg_class_find_attribute_by_name(struct tplg_pre_processor *tplg_pp, + snd_config_t *class, const char *name); +bool tplg_class_is_attribute_mandatory(const char *attr, snd_config_t *class_cfg); +bool tplg_class_is_attribute_immutable(const char *attr, snd_config_t *class_cfg); +bool tplg_class_is_attribute_unique(const char *attr, snd_config_t *class_cfg); +const char *tplg_class_get_unique_attribute_name(struct tplg_pre_processor *tplg_pp, + snd_config_t *class); +snd_config_type_t tplg_class_get_attribute_type(struct tplg_pre_processor *tplg_pp, + snd_config_t *attr); +const char *tplg_class_get_attribute_token_ref(struct tplg_pre_processor *tplg_pp, + snd_config_t *class, const char *attr_name); +long tplg_class_attribute_valid_tuple_value(struct tplg_pre_processor *tplg_pp, + snd_config_t *class, snd_config_t *attr); + +/* config helpers */ +snd_config_t *tplg_find_config(snd_config_t *config, const char *name); +int tplg_config_make_add(snd_config_t **config, const char *id, snd_config_type_t type, + snd_config_t *parent); + +char *tplg_snprintf(char *fmt, ...); +#endif diff --git a/topology/topology.c b/topology/topology.c new file mode 100644 index 0000000..312499f --- /dev/null +++ b/topology/topology.c @@ -0,0 +1,556 @@ +/* + Copyright(c) 2019 Red Hat Inc. + Copyright(c) 2014-2015 Intel Corporation + Copyright(c) 2010-2011 Texas Instruments Incorporated, + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of version 2 of the GNU General Public License as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + The full GNU General Public License is included in this distribution + in the file called LICENSE.GPL. +*/ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "gettext.h" +#ifdef ENABLE_NLS +#include +#endif +#include "version.h" +#include "topology.h" + +bool pre_process_config = false; + +static snd_output_t *log; + +static void usage(const char *name) +{ + printf( +_("Usage: %s [OPTIONS]...\n" +"\n" +"-h, --help help\n" +"-c, --compile=FILE compile configuration file\n" +"-p, --pre-process pre-process Topology2.0 configuration file before compilation\n" +"-P, --pre-process=FILE pre-process Topology2.0 configuration file\n" +"-d, --decode=FILE decode binary topology file\n" +"-n, --normalize=FILE normalize configuration file\n" +"-u, --dump=FILE dump (reparse) configuration file\n" +"-v, --verbose=LEVEL set verbosity level (0...1)\n" +"-o, --output=FILE set output file\n" +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION +"-D, --define=ARGS define variables (VAR1=VAL1[,VAR2=VAL2] ...)\n" +" (may be used multiple times)\n" +"-I, --inc-dir=DIR set include path\n" +#endif +"-s, --sort sort the identifiers in the normalized output\n" +"-g, --group save configuration by group indexes\n" +"-x, --nocheck save configuration without additional integrity checks\n" +"-z, --dapm-nosort do not sort the DAPM widgets\n" +"-V, --version print version\n" +), name); +} + +static void version(const char *name) +{ + printf( +_("%s version %s\n" +"libasound version %s\n" +"libatopology version %s\n" +), name, SND_UTIL_VERSION_STR, + snd_asoundlib_version(), snd_tplg_version()); +} + +static int load(const char *source_file, void **dst, size_t *dst_size) +{ + int fd; + void *buf, *buf2; + size_t size, pos; + ssize_t r; + + if (strcmp(source_file, "-") == 0) { + fd = fileno(stdin); + } else { + fd = open(source_file, O_RDONLY); + if (fd < 0) { + fprintf(stderr, _("Unable to open input file '%s': %s\n"), + source_file, strerror(errno)); + return 1; + } + } + + size = 16*1024; + pos = 0; + buf = malloc(size); + if (buf == NULL) + goto _nomem; + while (1) { + r = read(fd, buf + pos, size - pos); + if (r < 0 && (errno == EAGAIN || errno == EINTR)) + continue; + if (r <= 0) + break; + pos += r; + size += 8*1024; + buf2 = realloc(buf, size); + if (buf2 == NULL) + goto _nomem; + buf = buf2; + } + if (r < 0) { + fprintf(stderr, _("Read error: %s\n"), strerror(errno)); + goto _err; + } + + if (fd != fileno(stdin)) + close(fd); + + *dst = buf; + *dst_size = pos; + return 0; + +_nomem: + fprintf(stderr, _("No enough memory\n")); +_err: + if (fd != fileno(stdin)) + close(fd); + free(buf); + return 1; +} + +static int load_topology(snd_tplg_t **tplg, char *config, + size_t config_size, int cflags) +{ + int err; + + *tplg = snd_tplg_create(cflags); + if (*tplg == NULL) { + fprintf(stderr, _("failed to create new topology context\n")); + return 1; + } + + err = snd_tplg_load(*tplg, config, config_size); + if (err < 0) { + fprintf(stderr, _("Unable to load configuration: %s\n"), + snd_strerror(-err)); + snd_tplg_free(*tplg); + return 1; + } + + return 0; +} + +static int save(const char *output_file, void *buf, size_t size) +{ + char *fname = NULL; + int fd; + ssize_t r; + + if (strcmp(output_file, "-") == 0) { + fd = fileno(stdout); + } else { + fname = alloca(strlen(output_file) + 5); + strcpy(fname, output_file); + strcat(fname, ".new"); + fd = open(fname, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); + if (fd < 0) { + fprintf(stderr, _("Unable to open output file '%s': %s\n"), + fname, strerror(errno)); + return 1; + } + } + + r = 0; + while (size > 0) { + r = write(fd, buf, size); + if (r < 0 && (errno == EAGAIN || errno == EINTR)) + continue; + if (r < 0) + break; + size -= r; + buf += r; + } + + if (r < 0) { + fprintf(stderr, _("Write error: %s\n"), strerror(errno)); + if (fd != fileno(stdout)) { + if (fname && remove(fname)) + fprintf(stderr, _("Unable to remove file %s: %s\n"), + fname, strerror(errno)); + close(fd); + } + return 1; + } + + if (fd != fileno(stdout)) + close(fd); + + if (fname && rename(fname, output_file)) { + fprintf(stderr, _("Unable to rename file '%s' to '%s': %s\n"), + fname, output_file, strerror(errno)); + return 1; + } + + return 0; +} + +static int dump(const char *source_file, const char *output_file, int cflags, int sflags) +{ + snd_tplg_t *tplg; + char *config, *text; + size_t size; + int err; + + err = load(source_file, (void **)&config, &size); + if (err) + return err; + err = load_topology(&tplg, config, size, cflags); + free(config); + if (err) + return err; + err = snd_tplg_save(tplg, &text, sflags); + snd_tplg_free(tplg); + if (err < 0) { + fprintf(stderr, _("Unable to save parsed configuration: %s\n"), + snd_strerror(-err)); + return 1; + } + err = save(output_file, text, strlen(text)); + free(text); + return err; +} + +static char *get_inc_path(const char *filename) +{ + const char *s = strrchr(filename, '/'); + char *r = strdup(filename); + if (r) { + if (s) + r[s - filename] = '\0'; + else if (r[0]) + strcpy(r, "."); + } + return r; +} + +static int pre_process_run(struct tplg_pre_processor **tplg_pp, + const char *source_file, const char *output_file, + const char *pre_processor_defs, const char *include_path) +{ + size_t config_size; + char *config, *inc_path; + snd_output_type_t output_type; + int err; + + err = load(source_file, (void **)&config, &config_size); + if (err) + return err; + + /* init pre-processor */ + output_type = output_file == NULL ? SND_OUTPUT_BUFFER : SND_OUTPUT_STDIO; + err = init_pre_processor(tplg_pp, output_type, output_file); + if (err < 0) { + fprintf(stderr, _("failed to init pre-processor for Topology2.0\n")); + free(config); + return err; + } + + /* pre-process conf file */ + if (!include_path) + inc_path = get_inc_path(source_file); + else + inc_path = strdup(include_path); + err = pre_process(*tplg_pp, config, config_size, pre_processor_defs, inc_path); + free(inc_path); + + if (err < 0) + free_pre_processor(*tplg_pp); + free(config); + return err; +} + +/* Convert Topology2.0 conf to the existing conf syntax */ +static int pre_process_conf(const char *source_file, const char *output_file, + const char *pre_processor_defs, const char *include_path) +{ + struct tplg_pre_processor *tplg_pp; + int err; + + err = pre_process_run(&tplg_pp, source_file, output_file, + pre_processor_defs, include_path); + if (err < 0) + return err; + + /* free pre-processor */ + free_pre_processor(tplg_pp); + return err; +} + +static int compile(const char *source_file, const char *output_file, int cflags, + const char *pre_processor_defs, const char *include_path) +{ + struct tplg_pre_processor *tplg_pp = NULL; + snd_tplg_t *tplg; + char *config; + void *bin; + size_t config_size, size; + int err; + + err = load(source_file, (void **)&config, &config_size); + if (err) + return err; + + /* pre-process before compiling */ + if (pre_process_config) { + char *pconfig; + size_t size; + + err = pre_process_run(&tplg_pp, source_file, NULL, + pre_processor_defs, include_path); + if (err < 0) + return err; + + /* load topology */ + size = snd_output_buffer_string(tplg_pp->output, &pconfig); + err = load_topology(&tplg, pconfig, size, cflags); + + /* free pre-processor */ + free_pre_processor(tplg_pp); + } else { + err = load_topology(&tplg, config, config_size, cflags); + } + free(config); + if (err) + return err; + err = snd_tplg_build_bin(tplg, &bin, &size); + snd_tplg_free(tplg); + if (err < 0 || size == 0) { + fprintf(stderr, _("failed to compile context %s: %s\n"), + source_file, snd_strerror(-err)); + return 1; + } + err = save(output_file, bin, size); + free(bin); + return err; +} + +static int decode(const char *source_file, const char *output_file, + int cflags, int dflags, int sflags) +{ + snd_tplg_t *tplg; + void *bin; + char *text; + size_t size; + int err; + + if (load(source_file, &bin, &size)) + return 1; + tplg = snd_tplg_create(cflags); + if (tplg == NULL) { + fprintf(stderr, _("failed to create new topology context\n")); + return 1; + } + err = snd_tplg_decode(tplg, bin, size, dflags); + free(bin); + if (err < 0) { + snd_tplg_free(tplg); + fprintf(stderr, _("failed to decode context %s: %s\n"), + source_file, snd_strerror(-err)); + return 1; + } + err = snd_tplg_save(tplg, &text, sflags); + snd_tplg_free(tplg); + if (err < 0) { + fprintf(stderr, _("Unable to save parsed configuration: %s\n"), + snd_strerror(-err)); + return 1; + } + err = save(output_file, text, strlen(text)); + free(text); + return err; +} + +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION +static int add_define(char **defs, char *d) +{ + size_t len = (*defs ? strlen(*defs) : 0) + strlen(d) + 2; + char *m = realloc(*defs, len); + if (m) { + if (*defs) { + strcat(m, ","); + strcat(m, d); + } else { + strcpy(m, d); + } + *defs = m; + return 0; + } + return 1; +} +#endif + +int main(int argc, char *argv[]) +{ + static const char short_options[] = "hc:d:n:u:v:o:pP:sgxzV" +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + "D:I:" +#endif + ; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"verbose", 1, NULL, 'v'}, + {"compile", 1, NULL, 'c'}, + {"pre-process", 1, NULL, 'p'}, + {"decode", 1, NULL, 'd'}, + {"normalize", 1, NULL, 'n'}, + {"dump", 1, NULL, 'u'}, + {"output", 1, NULL, 'o'}, +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + {"define", 1, NULL, 'D'}, + {"inc-dir", 1, NULL, 'I'}, +#endif + {"sort", 0, NULL, 's'}, + {"group", 0, NULL, 'g'}, + {"nocheck", 0, NULL, 'x'}, + {"dapm-nosort", 0, NULL, 'z'}, + {"version", 0, NULL, 'V'}, + {0, 0, 0, 0}, + }; + char *source_file = NULL; + char *output_file = NULL; + const char *inc_path = NULL; + char *pre_processor_defs = NULL; + int c, err, op = 'c', cflags = 0, dflags = 0, sflags = 0, option_index; + +#ifdef ENABLE_NLS + setlocale(LC_ALL, ""); + textdomain(PACKAGE); +#endif + + err = snd_output_stdio_attach(&log, stderr, 0); + assert(err >= 0); + + while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return 0; + case 'v': + cflags |= SND_TPLG_CREATE_VERBOSE; + break; + case 'z': + cflags |= SND_TPLG_CREATE_DAPM_NOSORT; + break; + case 'c': + case 'd': + case 'n': + case 'u': + if (source_file) { + fprintf(stderr, _("Cannot combine operations (compile, normalize, pre-process, dump)\n")); + return 1; + } + source_file = optarg; + op = c; + break; + case 'o': + output_file = optarg; + break; + case 's': + sflags |= SND_TPLG_SAVE_SORT; + break; + case 'P': + op = 'P'; + source_file = optarg; + break; +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + case 'I': + inc_path = optarg; + break; +#endif + case 'p': + pre_process_config = true; + break; + case 'g': + sflags |= SND_TPLG_SAVE_GROUPS; + break; + case 'x': + sflags |= SND_TPLG_SAVE_NOCHECK; + break; +#if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION + case 'D': + if (add_define(&pre_processor_defs, optarg)) { + fprintf(stderr, _("No enough memory")); + return 1; + } + break; +#endif + case 'V': + version(argv[0]); + return 0; + default: + fprintf(stderr, _("Try `%s --help' for more information.\n"), argv[0]); + return 1; + } + } + + if (source_file == NULL || output_file == NULL) { + usage(argv[0]); + return 1; + } + + if ((cflags & SND_TPLG_CREATE_VERBOSE) != 0 && + output_file && strcmp(output_file, "-") == 0) { + fprintf(stderr, _("Invalid mix of verbose level and output to stdout.\n")); + return 1; + } + + if (op == 'n') { + if (sflags != 0 && sflags != SND_TPLG_SAVE_SORT) { + fprintf(stderr, _("Wrong parameters for the normalize operation!\n")); + return 1; + } + /* normalize has predefined output */ + sflags = SND_TPLG_SAVE_SORT; + } + + switch (op) { + case 'c': + err = compile(source_file, output_file, cflags, pre_processor_defs, inc_path); + break; + case 'd': + err = decode(source_file, output_file, cflags, dflags, sflags); + break; + case 'P': + err = pre_process_conf(source_file, output_file, pre_processor_defs, inc_path); + break; + default: + err = dump(source_file, output_file, cflags, sflags); + break; + } + + snd_output_close(log); + free(pre_processor_defs); + return err ? 1 : 0; +} diff --git a/topology/topology.h b/topology/topology.h new file mode 100644 index 0000000..3a09c20 --- /dev/null +++ b/topology/topology.h @@ -0,0 +1,39 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef __TOPOLOGY_H +#define __TOPOLOGY_H + +#include + +/* pre_processor */ +struct tplg_pre_processor { + snd_config_t *input_cfg; + snd_config_t *output_cfg; + snd_output_t *output; + snd_output_t *dbg_output; + snd_config_t *current_obj_cfg; + snd_config_t *define_cfg; + snd_config_t *define_cfg_merged; + char *inc_path; +}; + +int pre_process(struct tplg_pre_processor *tplg_pp, char *config, size_t config_size, + const char *pre_processor_defs, const char *inc_path); +int init_pre_processor(struct tplg_pre_processor **tplg_pp, snd_output_type_t type, + const char *output_file); +void free_pre_processor(struct tplg_pre_processor *tplg_pp); +#endif diff --git a/utils/Makefile.am b/utils/Makefile.am new file mode 100644 index 0000000..083e5b4 --- /dev/null +++ b/utils/Makefile.am @@ -0,0 +1,4 @@ +EXTRA_DIST = buildrpm + +rpm: buildrpm alsa-lib.spec + VERSION=$(VERSION) $(srcdir)/buildrpm diff --git a/utils/alsa-utils.spec.in b/utils/alsa-utils.spec.in new file mode 100644 index 0000000..8daf3ba --- /dev/null +++ b/utils/alsa-utils.spec.in @@ -0,0 +1,76 @@ +%define ver @VERSION@ +%define rel 1 + +Summary: Advanced Linux Sound Architecture (ALSA) - Utils +Name: alsa-utils +Version: %ver +Release: %rel +Copyright: GPL +Group: System/Libraries +Source: ftp://ftp.alsa-project.org/pub/utils/alsa-utils-%{ver}.tar.bz2 +BuildRoot: %{_tmppath}/%{name}-%{version}-root +URL: http://www.alsa-project.org +Requires: alsa-lib ncurses +BuildRequires: alsa-lib-devel ncurses-devel gettext + +%description + +Advanced Linux Sound Architecture (ALSA) - Utils + +%changelog +* Sun Oct 1 2006 Jaroslav Kysela +- add gettext to BuildRequires +- add more files (see alsa bug#2139) + +* Tue Nov 25 2003 Ronny V. Vindenes +- include all manpages + +* Thu Mar 6 2003 Ronny V. Vindenes + +- removed wrongly included doc file +- changed BuildRoot from /var/tmp to _tmppath +- use standard rpm macros for build & install section +- updated dependencies + +* Tue Nov 20 2001 Jaroslav Kysela + +- changed BuildRoot from /tmp to /var/tmp +- _prefix and _mandir macros are used for configure and mkdir +- DESTDIR is used for make install + +* Sun Nov 11 2001 Miroslav Benes + +- dangerous command "rpm -rf $RPM_BUILD_ROOT" checks $RPM_BUILD_ROOT variable +- unset key "Docdir" - on some new systems are documentation in /usr/share/doc + +* Mon May 28 1998 Helge Jensen + +- Made SPEC file + +%prep +%setup +%build +%configure +make + +%install +[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT +%makeinstall + +%clean +[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT + +%files +%defattr(-, root, root) + +%doc ChangeLog COPYING README + +%{_prefix}/sbin/* +%{_prefix}/bin/* +%{_mandir}/man?/* +%{_mandir}/fr/man?/* +%{_prefix}/share/alsa/speaker-test/* +%{_prefix}/share/locale/ja/LC_MESSAGES/* +%{_prefix}/share/locale/ru/LC_MESSAGES/* +%{_prefix}/share/man/fr/man8/alsaconf.8.gz +%{_prefix}/share/sounds/alsa/* diff --git a/utils/buildrpm b/utils/buildrpm new file mode 100644 index 0000000..3a9ab86 --- /dev/null +++ b/utils/buildrpm @@ -0,0 +1,44 @@ +#!/bin/bash + +source=. +version=`cat $source/../version` +package=$source/../alsa-utils-$version.tar.bz2 +packagedir=/usr/src/redhat +xrpmbuild=rpm +rpmbuild --usage 2> /dev/null > /dev/null && xrpmbuild=rpmbuild + +# SuSE path +if [ -d /usr/src/packages ]; then + packagedir=/usr/src/packages +fi + +make -C .. clean +make -C .. dist + +if [ ! -r $package ]; then + echo "Error: wrong package: $package" + exit 1 +fi + +cp -fv $package ${packagedir}/SOURCES + +if [ ! -r $source/buildrpm ]; then + echo "Error: invalid directory: $source" + exit 1 +fi + +if [ ! -d ${packagedir} ]; then + echo "Error: ${packagedir} directory not found" + exit 1 +fi + +if [ ! -r $source/alsadriver.spec ]; then + cd $source/.. + ./configure + cd utils +fi + +cp -fv $source/alsa-utils.spec ${packagedir}/SPECS +cd ${packagedir}/SPECS +$xrpmbuild -ba alsa-utils.spec +cd ${packagedir}

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