From bfbf13ff3cbd31ba7719a24a823cdc097a03826d Mon Sep 17 00:00:00 2001
From: Victor Gaydov <victor@enise.org>
Date: Sat, 24 Aug 2024 15:40:38 +0300
Subject: [PATCH] [wip] [api] [cli] gh-608: Formats and sub-formats

API
---

  TODO

CLI
---

  1. --io-encoding option now has form
    <format>@<subformat>/<rate>/<channels>

  E.g.: pcm@s16/44100/stereo
  (whether sub-format is allowed or required depends
   on format)

  2. --input-format/--output-format options are removed,
     their function is now handled by <format> field
     of --io-encoding

  E.g.:
    --output file://- --io-encoding wav@s24/48000/stereo
     or
    --input file://- --io-encoding wav/-/-

  3. --print-supported is updated to discover and list all
     available sub-formats (divided into logical groups).

Docs
----

  TODO

Internals
---------

Introduce concept of format (e.g. PCM, FLAC) and sub-format
(e.g. s16). Support formats and sub-formats in all sndio
backends.

roc_audio:

- SampleFormat => Format
- Formats: Format_Pcm, Format_Wav, Format_Custom
- PcmFormat => PcmSubformat
- SampleSpec: set_format(), set_custom_format(),
  set_pcm_subformat(), set_custom_subformat()
- SampleSpec: is_valid() => is_complete()
- Sample_RawFormat => PcmSubformat_Raw

roc_sndio:

- File formats are now *not* drivers. All file formats
  are handles by special "file://" driver, i.e.
  URI scheme is now always equal to driver.
- Supported file formats and sub-formats are
  discovered from backends separately from drivers.
- For discovery, we use DriverInfo and FormatInfo
  structs.

- IoConfig is empty by default; frame length and latency
  are zero.
- Each backend may use its own defaults for IoConfig.
- We can retrieve actually selected config using
  sample_spec() and frame_length() methods of IDevice.

- SoxBackend: remove file support, allow only devices
  (now we use sndfile for files)
- SoxBackend: support PCM format and sub-formats
- PulseaudioBackend: support PCM format and sub-formats
- SndfileBackend: support formats and sub-formats,
  map to sndfile major type and sub-type
- WavBackend: support WAV format and PCM sub-formats

- Unification of sndio backends
- Bug-fixes in format handling in sndfile backend
---
 .fmtignore                                    |    2 +-
 SConstruct                                    |    2 +-
 docs/man/roc-copy.1                           |   11 +-
 docs/man/roc-recv.1                           |   28 +-
 docs/man/roc-send.1                           |   23 +-
 docs/sphinx/manuals/roc_copy.rst              |    7 +-
 docs/sphinx/manuals/roc_recv.rst              |   32 +-
 docs/sphinx/manuals/roc_send.rst              |   29 +-
 src/internal_modules/roc_audio/beep_plc.cpp   |    4 +-
 .../roc_audio/builtin_resampler.cpp           |    4 +-
 .../roc_audio/channel_mapper_reader.cpp       |    4 +-
 .../roc_audio/channel_mapper_writer.cpp       |    4 +-
 .../roc_audio/channel_set_format.cpp          |   60 +-
 .../roc_audio/decimation_resampler.cpp        |    4 +-
 .../roc_audio/depacketizer.cpp                |    4 +-
 src/internal_modules/roc_audio/fanout.cpp     |    3 +-
 src/internal_modules/roc_audio/format.cpp     |   60 +
 src/internal_modules/roc_audio/format.h       |   95 +
 src/internal_modules/roc_audio/mixer.cpp      |    4 +-
 src/internal_modules/roc_audio/packetizer.cpp |    4 +-
 .../roc_audio/pcm_decoder.cpp                 |    2 +-
 .../roc_audio/pcm_encoder.cpp                 |    2 +-
 src/internal_modules/roc_audio/pcm_format.cpp | 1758 +++++++++--------
 src/internal_modules/roc_audio/pcm_mapper.cpp |   14 +-
 src/internal_modules/roc_audio/pcm_mapper.h   |   14 +-
 .../roc_audio/pcm_mapper_reader.cpp           |   12 +-
 .../roc_audio/pcm_mapper_reader.h             |    2 +-
 .../roc_audio/pcm_mapper_writer.cpp           |   12 +-
 .../roc_audio/pcm_mapper_writer.h             |    2 +-
 .../{pcm_format.h => pcm_subformat.h}         |  220 ++-
 ...pcm_format_gen.py => pcm_subformat_gen.py} |   94 +-
 .../roc_audio/pcm_subformat_rw.h              |   72 +
 src/internal_modules/roc_audio/plc_reader.cpp |    6 +-
 .../roc_audio/resampler_reader.cpp            |    4 +-
 .../roc_audio/resampler_writer.cpp            |    4 +-
 src/internal_modules/roc_audio/sample.cpp     |    3 +-
 src/internal_modules/roc_audio/sample.h       |    4 +-
 .../roc_audio/sample_format.cpp               |   27 -
 .../roc_audio/sample_format.h                 |   39 -
 .../roc_audio/sample_spec.cpp                 |  346 +++-
 src/internal_modules/roc_audio/sample_spec.h  |  142 +-
 .../roc_audio/sample_spec_format.cpp          |   11 +-
 .../roc_audio/sample_spec_parse.rl            |  124 +-
 .../roc_audio/speex_resampler.cpp             |    4 +-
 src/internal_modules/roc_core/backtrace.h     |    2 +-
 src/internal_modules/roc_core/string_list.cpp |  227 ++-
 src/internal_modules/roc_core/string_list.h   |   54 +-
 .../roc_core/target_posix/roc_core/time.cpp   |    4 +-
 .../roc_dbgio/print_supported.cpp             |  213 +-
 .../target_posix/roc_dbgio/temp_file.cpp      |    2 +-
 src/internal_modules/roc_pipeline/config.h    |    2 +-
 .../roc_pipeline/receiver_loop.cpp            |    6 +
 .../roc_pipeline/receiver_loop.h              |    1 +
 .../roc_pipeline/receiver_session.cpp         |   12 +-
 .../roc_pipeline/receiver_source.cpp          |    8 +-
 .../roc_pipeline/receiver_source.h            |    3 +
 .../roc_pipeline/sender_loop.cpp              |    6 +
 .../roc_pipeline/sender_loop.h                |    1 +
 .../roc_pipeline/sender_session.cpp           |   12 +-
 .../roc_pipeline/sender_sink.cpp              |    8 +-
 .../roc_pipeline/sender_sink.h                |    3 +
 .../roc_pipeline/transcoder_sink.cpp          |   12 +-
 .../roc_pipeline/transcoder_sink.h            |    3 +
 .../roc_pipeline/transcoder_source.cpp        |   12 +-
 .../roc_pipeline/transcoder_source.h          |    3 +
 src/internal_modules/roc_rtp/encoding.cpp     |   21 +-
 src/internal_modules/roc_rtp/encoding.h       |    2 +-
 src/internal_modules/roc_rtp/encoding_map.cpp |   43 +-
 .../roc_sndio/backend_dispatcher.cpp          |  267 ++-
 .../roc_sndio/backend_dispatcher.h            |   24 +-
 .../roc_sndio/backend_map.cpp                 |   39 +-
 src/internal_modules/roc_sndio/backend_map.h  |   12 +-
 src/internal_modules/roc_sndio/driver.cpp     |   31 -
 src/internal_modules/roc_sndio/driver.h       |  103 +-
 src/internal_modules/roc_sndio/ibackend.h     |   17 +-
 src/internal_modules/roc_sndio/idevice.h      |    4 +
 src/internal_modules/roc_sndio/io_config.h    |    8 +-
 src/internal_modules/roc_sndio/io_pump.cpp    |   77 +-
 src/internal_modules/roc_sndio/io_pump.h      |    6 +-
 .../roc_sndio/pulseaudio_backend.cpp          |   52 +-
 .../roc_sndio/pulseaudio_backend.h            |   15 +-
 .../roc_sndio/pulseaudio_device.cpp           |  170 +-
 .../roc_sndio/pulseaudio_device.h             |    9 +-
 .../roc_sndio/sndfile_backend.cpp             |  131 +-
 .../roc_sndio/sndfile_backend.h               |   15 +-
 .../roc_sndio/sndfile_helpers.cpp             |  349 ++++
 .../roc_sndio/sndfile_helpers.h               |   43 +
 .../target_sndfile/roc_sndio/sndfile_sink.cpp |  313 +--
 .../target_sndfile/roc_sndio/sndfile_sink.h   |   19 +-
 .../roc_sndio/sndfile_source.cpp              |  119 +-
 .../target_sndfile/roc_sndio/sndfile_source.h |   16 +-
 .../roc_sndio/sndfile_tables.cpp              |   33 +-
 .../target_sndfile/roc_sndio/sndfile_tables.h |   38 +-
 .../target_sox/roc_sndio/sox_backend.cpp      |  241 +--
 .../target_sox/roc_sndio/sox_backend.h        |   18 +-
 .../target_sox/roc_sndio/sox_sink.cpp         |  262 +--
 .../roc_sndio/target_sox/roc_sndio/sox_sink.h |   23 +-
 .../target_sox/roc_sndio/sox_source.cpp       |  274 +--
 .../target_sox/roc_sndio/sox_source.h         |   25 +-
 .../roc_sndio/wav_backend.cpp                 |   88 +-
 src/internal_modules/roc_sndio/wav_backend.h  |   15 +-
 src/internal_modules/roc_sndio/wav_sink.cpp   |  195 +-
 src/internal_modules/roc_sndio/wav_sink.h     |   12 +-
 src/internal_modules/roc_sndio/wav_source.cpp |  154 +-
 src/internal_modules/roc_sndio/wav_source.h   |   13 +-
 .../roc_status/code_to_str.cpp                |    2 +
 src/internal_modules/roc_status/status_code.h |   12 +-
 .../examples/basic_receiver_pulseaudio.c      |    3 +-
 .../examples/basic_receiver_wav_file.c        |    3 +-
 .../examples/basic_sender_pulseaudio.c        |    3 +-
 .../examples/basic_sender_sine_wave.c         |    3 +-
 src/public_api/examples/plugin_plc.c          |    3 +-
 .../examples/send_recv_1_sender_2_receivers.c |    6 +-
 .../examples/send_recv_2_senders_1_receiver.c |    6 +-
 src/public_api/examples/send_recv_multicast.c |    6 +-
 src/public_api/examples/send_recv_rtp.c       |    6 +-
 .../examples/send_recv_rtp_rtcp_fec.c         |    6 +-
 src/public_api/include/roc/config.h           |   87 +-
 src/public_api/include/roc/plugin.h           |    4 +-
 src/public_api/src/adapters.cpp               |  212 +-
 src/public_api/src/adapters.h                 |    6 +-
 src/tests/public_api/test_context.cpp         |    6 +-
 src/tests/public_api/test_helpers/context.h   |   23 +-
 src/tests/public_api/test_helpers/utils.h     |    2 -
 .../test_loopback_encoder_2_decoder.cpp       |   19 +-
 .../test_loopback_sender_2_receiver.cpp       |  180 +-
 src/tests/public_api/test_plugin_plc.cpp      |   25 +-
 src/tests/public_api/test_receiver.cpp        |    3 +-
 .../public_api/test_receiver_decoder.cpp      |    6 +-
 src/tests/public_api/test_sender.cpp          |    3 +-
 src/tests/public_api/test_sender_encoder.cpp  |    3 +-
 .../roc_audio/test_channel_mapper_reader.cpp  |   40 +-
 .../roc_audio/test_channel_mapper_writer.cpp  |   28 +-
 src/tests/roc_audio/test_channel_set.cpp      |   20 +-
 src/tests/roc_audio/test_depacketizer.cpp     |    4 +-
 src/tests/roc_audio/test_fanout.cpp           |    2 +-
 .../roc_audio/test_frame_encoder_decoder.cpp  |   18 +-
 src/tests/roc_audio/test_freq_estimator.cpp   |    2 +-
 src/tests/roc_audio/test_mixer.cpp            |   18 +-
 src/tests/roc_audio/test_packetizer.cpp       |    4 +-
 src/tests/roc_audio/test_pcm_mapper.cpp       |   70 +-
 .../roc_audio/test_pcm_mapper_reader.cpp      |   68 +-
 .../roc_audio/test_pcm_mapper_writer.cpp      |   54 +-
 src/tests/roc_audio/test_pcm_samples.cpp      |   12 +-
 src/tests/roc_audio/test_plc_reader.cpp       |   34 +-
 src/tests/roc_audio/test_profiler.cpp         |    2 +-
 src/tests/roc_audio/test_resampler.cpp        |   46 +-
 src/tests/roc_audio/test_sample_spec.cpp      |  443 +++--
 .../test_samples/generate_samples.py          |    2 +-
 .../roc_audio/test_samples/pcm_float32_be.h   |    2 +-
 .../roc_audio/test_samples/pcm_float32_le.h   |    2 +-
 .../roc_audio/test_samples/pcm_sint16_be.h    |    2 +-
 .../roc_audio/test_samples/pcm_sint16_le.h    |    2 +-
 .../roc_audio/test_samples/pcm_sint24_be.h    |    2 +-
 .../roc_audio/test_samples/pcm_sint24_le.h    |    2 +-
 .../roc_audio/test_samples/pcm_sint32_be.h    |    2 +-
 .../roc_audio/test_samples/pcm_sint32_le.h    |    2 +-
 .../roc_audio/test_samples/pcm_sint8_be.h     |    2 +-
 .../roc_audio/test_samples/pcm_sint8_le.h     |    2 +-
 .../roc_audio/test_samples/pcm_uint16_be.h    |    2 +-
 .../roc_audio/test_samples/pcm_uint16_le.h    |    2 +-
 .../roc_audio/test_samples/pcm_uint24_be.h    |    2 +-
 .../roc_audio/test_samples/pcm_uint24_le.h    |    2 +-
 .../roc_audio/test_samples/pcm_uint32_be.h    |    2 +-
 .../roc_audio/test_samples/pcm_uint32_le.h    |    2 +-
 .../roc_audio/test_samples/pcm_uint8_be.h     |    2 +-
 .../roc_audio/test_samples/pcm_uint8_le.h     |    2 +-
 .../roc_audio/test_samples/sample_info.h      |    4 +-
 src/tests/roc_audio/test_watchdog.cpp         |    2 +-
 src/tests/roc_core/test_string_list.cpp       |   12 +-
 src/tests/roc_packet/test_delayed_reader.cpp  |    2 +-
 .../bench_pipeline_loop_contention.cpp        |    2 +-
 .../bench_pipeline_loop_peak_load.cpp         |    2 +-
 .../roc_pipeline/test_helpers/mock_sink.h     |    4 +
 .../roc_pipeline/test_helpers/mock_source.h   |    4 +
 .../test_loopback_sink_2_source.cpp           |   26 +-
 src/tests/roc_pipeline/test_pipeline_loop.cpp |    2 +-
 .../roc_pipeline/test_receiver_source.cpp     |   44 +-
 src/tests/roc_pipeline/test_sender_sink.cpp   |   44 +-
 .../roc_pipeline/test_transcoder_sink.cpp     |    8 +-
 .../roc_pipeline/test_transcoder_source.cpp   |    8 +-
 src/tests/roc_rtp/test_encoding.cpp           |   26 +-
 src/tests/roc_rtp/test_encoding_map.cpp       |   32 +-
 src/tests/roc_rtp/test_filter.cpp             |    2 +-
 src/tests/roc_rtp/test_link_meter.cpp         |    2 +-
 .../roc_rtp/test_timestamp_extractor.cpp      |    4 +-
 src/tests/roc_rtp/test_timestamp_injector.cpp |    6 +-
 src/tests/roc_sndio/test_backend_sink.cpp     |  136 --
 src/tests/roc_sndio/test_backend_source.cpp   |  231 ---
 src/tests/roc_sndio/test_helpers/mock_sink.h  |    4 +
 .../roc_sndio/test_helpers/mock_source.h      |   30 +-
 src/tests/roc_sndio/test_helpers/utils.h      |   20 +-
 src/tests/roc_sndio/test_io_pump.cpp          |   70 +-
 src/tests/roc_sndio/test_sinks.cpp            |  429 ++++
 src/tests/roc_sndio/test_sources.cpp          |  499 +++++
 src/tools/roc_copy/cmdline.ggo                |    3 +-
 src/tools/roc_copy/main.cpp                   |   30 +-
 src/tools/roc_recv/cmdline.ggo                |    5 +-
 src/tools/roc_recv/main.cpp                   |   76 +-
 src/tools/roc_send/cmdline.ggo                |    1 -
 src/tools/roc_send/main.cpp                   |   44 +-
 201 files changed, 6517 insertions(+), 4030 deletions(-)
 create mode 100644 src/internal_modules/roc_audio/format.cpp
 create mode 100644 src/internal_modules/roc_audio/format.h
 rename src/internal_modules/roc_audio/{pcm_format.h => pcm_subformat.h} (71%)
 rename src/internal_modules/roc_audio/{pcm_format_gen.py => pcm_subformat_gen.py} (92%)
 create mode 100644 src/internal_modules/roc_audio/pcm_subformat_rw.h
 delete mode 100644 src/internal_modules/roc_audio/sample_format.cpp
 delete mode 100644 src/internal_modules/roc_audio/sample_format.h
 delete mode 100644 src/internal_modules/roc_sndio/driver.cpp
 create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.cpp
 create mode 100644 src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.h
 delete mode 100644 src/tests/roc_sndio/test_backend_sink.cpp
 delete mode 100644 src/tests/roc_sndio/test_backend_source.cpp
 create mode 100644 src/tests/roc_sndio/test_sinks.cpp
 create mode 100644 src/tests/roc_sndio/test_sources.cpp

diff --git a/.fmtignore b/.fmtignore
index ead144511..84eb45f99 100644
--- a/.fmtignore
+++ b/.fmtignore
@@ -1,5 +1,5 @@
 src/internal_modules/roc_audio/channel_tables.cpp
-src/internal_modules/roc_audio/pcm_format.cpp
+src/internal_modules/roc_audio/pcm_subformat.cpp
 src/internal_modules/roc_core/macro_helpers.h
 src/internal_modules/roc_core/target_libatomic_ops/roc_core/atomic_ops.h
 src/public_api/include/roc/version.h
diff --git a/SConstruct b/SConstruct
index 233acf770..ac2e8c93c 100644
--- a/SConstruct
+++ b/SConstruct
@@ -729,10 +729,10 @@ env['ROC_MODULES'] = [
     'roc_audio',
     'roc_rtp',
     'roc_rtcp',
+    'roc_sdp',
     'roc_netio',
     'roc_sndio',
     'roc_pipeline',
-    'roc_sdp',
     'roc_ctl',
     'roc_node',
 ]
diff --git a/docs/man/roc-copy.1 b/docs/man/roc-copy.1
index 1eaf4454a..e0c0f1603 100644
--- a/docs/man/roc-copy.1
+++ b/docs/man/roc-copy.1
@@ -60,15 +60,12 @@ List supported protocols, formats, etc.
 .BI \-i\fP,\fB  \-\-input\fB= FILE_URI
 Input file URI
 .TP
-.BI \-\-input\-format\fB= FILE_FORMAT
-Force input file format
+.BI \-\-input\-encoding\fB= IO_ENCODING
+Input file encoding
 .TP
 .BI \-o\fP,\fB  \-\-output\fB= FILE_URI
 Output file URI
 .TP
-.BI \-\-output\-format\fB= FILE_FORMAT
-Force output file format
-.TP
 .BI \-\-output\-encoding\fB= IO_ENCODING
 Output file encoding
 .TP
@@ -150,7 +147,7 @@ Convert sample rate to 24\-bit 48k stereo:
 .sp
 .nf
 .ft C
-$ roc\-copy \-vv \-\-io\-encoding s24/48000/stereo \-i file:input.wav \-o file:output.wav
+$ roc\-copy \-vv \-\-io\-encoding pcm_s24/48000/stereo \-i file:input.wav \-o file:output.wav
 .ft P
 .fi
 .UNINDENT
@@ -162,7 +159,7 @@ Same, but drop output results instead of writing to file (useful for benchmarkin
 .sp
 .nf
 .ft C
-$ roc\-copy \-vv \-\-io\-encoding s24/48000/stereo \-i file:input.wav
+$ roc\-copy \-vv \-\-io\-encoding pcm_s24/48000/stereo \-i file:input.wav
 .ft P
 .fi
 .UNINDENT
diff --git a/docs/man/roc-recv.1 b/docs/man/roc-recv.1
index 2501d37fc..ce6287be1 100644
--- a/docs/man/roc-recv.1
+++ b/docs/man/roc-recv.1
@@ -66,9 +66,6 @@ Exit when last connection is closed (default=off)
 .BI \-o\fP,\fB  \-\-output\fB= IO_URI
 Output file or device URI
 .TP
-.BI \-\-output\-format\fB= FILE_FORMAT
-Force output file format
-.TP
 .BI \-\-io\-encoding\fB= IO_ENCODING
 Output device encoding
 .TP
@@ -82,10 +79,7 @@ Output frame length, TIME units
 .INDENT 0.0
 .TP
 .BI \-\-backup\fB= IO_URI
-Backup file or device URI (used as input when there are no connections)
-.TP
-.BI \-\-backup\-format\fB= FILE_FORMAT
-Force backup file format
+Backup file URI (used as input when there are no connections)
 .UNINDENT
 .SS Network options
 .INDENT 0.0
@@ -239,7 +233,7 @@ This option is useful when device supports multiple encodings, or specific file
 Where:
 .INDENT 0.0
 .IP \(bu 2
-\fBformat\fP defines sample precision and binary representation, e.g. \fBs16_le\fP stands for little\-endian signed 16\-bit integers
+\fBformat\fP defines sample precision and binary representation, e.g. \fBpcm_s16_le\fP stands for little\-endian signed 16\-bit integer PCM
 .IP \(bu 2
 \fBrate\fP defines sample rate in Hertz (number of samples per second), e.g. \fB48000\fP
 .IP \(bu 2
@@ -251,11 +245,11 @@ Any component may be set to special value \fB\-\fP, which means use default valu
 Examples:
 .INDENT 0.0
 .IP \(bu 2
-\fBs16/44100/mono\fP \-\- 16\-bit native\-endian integers, 44.1KHz, 1 channel
+\fBpcm_s16/44100/mono\fP \-\- 16\-bit native\-endian integers, 44.1KHz, 1 channel
 .IP \(bu 2
-\fBf32_le/48000/stereo\fP \-\- 32\-bit little\-endian floats, 48KHz, 2 channels
+\fBpcm_f32_le/48000/stereo\fP \-\- 32\-bit little\-endian floats, 48KHz, 2 channels
 .IP \(bu 2
-\fBs24_4be/\-/\-\fP \-\- 24\-bit PCM packed into 4\-byte big\-endian frames, default rate and channels
+\fBpcm_s24_4be/\-/\-\fP \-\- 24\-bit PCM packed into 4\-byte big\-endian frames, default rate and channels
 .UNINDENT
 .sp
 The list of supported formats and channel layouts can be retrieved using \fB\-\-list\-supported\fP option.
@@ -330,7 +324,7 @@ Where:
 .IP \(bu 2
 \fBid\fP is an arbitrary number in range 100..127, which should uniquely identify encoding on all related senders and receivers
 .IP \(bu 2
-\fBformat\fP defines sample precision and binary representation, e.g. \fBs16_le\fP stands for little\-endian signed 16\-bit integers
+\fBformat\fP defines sample precision and binary representation, e.g. \fBpcm_s24\fP stands for 24\-bit signed integer PCM
 .IP \(bu 2
 \fBrate\fP defines sample rate in Hertz (number of samples per second), e.g. \fB48000\fP
 .IP \(bu 2
@@ -340,9 +334,9 @@ Where:
 Examples:
 .INDENT 0.0
 .IP \(bu 2
-\fB101:s16_be/44100/mono\fP \-\- 16\-bit big\-endian integers, 44.1KHz, 1 channel
+\fB101:pcm_s24/44100/mono\fP \-\- 24\-bit network\-endian integers, 44.1KHz, 1 channel
 .IP \(bu 2
-\fB102:f32_le/48000/stereo\fP \-\- 32\-bit little\-endian floats, 48KHz, 2 channels
+\fB102:pcm_f32/48000/stereo\fP \-\- 32\-bit network\-endian floats, 48KHz, 2 channels
 .UNINDENT
 .sp
 The list of supported formats and channel layouts can be retrieved using \fB\-\-list\-supported\fP option.
@@ -650,7 +644,7 @@ Force specific encoding on the output device:
 .nf
 .ft C
 $ roc\-recv \-vv \-s rtp://0.0.0.0:10001 \e
-    \-\-output alsa://hw:1,0 \-\-io\-encoding s32/48000/stereo
+    \-\-output alsa://hw:1,0 \-\-io\-encoding pcm_s32/48000/stereo
 .ft P
 .fi
 .UNINDENT
@@ -662,7 +656,7 @@ Use specific encoding for network packets:
 .sp
 .nf
 .ft C
-$ roc\-send \-vv \-s rtp://192.168.0.3:10001 \-\-packet\-encoding 101:s32/48000/stereo
+$ roc\-send \-vv \-s rtp://192.168.0.3:10001 \-\-packet\-encoding 101:pcm_s24/48000/stereo
 .ft P
 .fi
 .UNINDENT
@@ -672,7 +666,7 @@ $ roc\-send \-vv \-s rtp://192.168.0.3:10001 \-\-packet\-encoding 101:s32/48000/
 .sp
 .nf
 .ft C
-$ roc\-recv \-vv \-s rtp://0.0.0.0:10001 \-\-packet\-encoding 101:s32/48000/stereo
+$ roc\-recv \-vv \-s rtp://0.0.0.0:10001 \-\-packet\-encoding 101:pcm_s24/48000/stereo
 .ft P
 .fi
 .UNINDENT
diff --git a/docs/man/roc-send.1 b/docs/man/roc-send.1
index 522409bc0..82c0b9c23 100644
--- a/docs/man/roc-send.1
+++ b/docs/man/roc-send.1
@@ -60,9 +60,6 @@ List supported protocols, formats, etc.
 .BI \-i\fP,\fB  \-\-input\fB= IO_URI
 Input file or device URI
 .TP
-.BI \-\-input\-format\fB= FILE_FORMAT
-Force input file format
-.TP
 .BI \-\-io\-encoding\fB= IO_ENCODING
 Input device encoding
 .TP
@@ -223,7 +220,7 @@ This option is useful when device supports multiple encodings. Note that I/O enc
 Where:
 .INDENT 0.0
 .IP \(bu 2
-\fBformat\fP defines sample precision and binary representation, e.g. \fBs16_le\fP stands for little\-endian signed 16\-bit integers
+\fBformat\fP defines sample precision and binary representation, e.g. \fBpcm_s16_le\fP stands for little\-endian signed 16\-bit integer PCM
 .IP \(bu 2
 \fBrate\fP defines sample rate in Hertz (number of samples per second), e.g. \fB48000\fP
 .IP \(bu 2
@@ -235,11 +232,11 @@ Any component may be set to special value \fB\-\fP, which means use default valu
 Examples:
 .INDENT 0.0
 .IP \(bu 2
-\fBs16/44100/mono\fP \-\- 16\-bit native\-endian integers, 44.1KHz, 1 channel
+\fBpcm_s16/44100/mono\fP \-\- 16\-bit native\-endian integers, 44.1KHz, 1 channel
 .IP \(bu 2
-\fBf32_le/48000/stereo\fP \-\- 32\-bit little\-endian floats, 48KHz, 2 channels
+\fBpcm_f32_le/48000/stereo\fP \-\- 32\-bit little\-endian floats, 48KHz, 2 channels
 .IP \(bu 2
-\fBs24_4be/\-/\-\fP \-\- 24\-bit PCM packed into 4\-byte big\-endian frames, default rate and channels
+\fBpcm_s24_4be/\-/\-\fP \-\- 24\-bit PCM packed into 4\-byte big\-endian frames, default rate and channels
 .UNINDENT
 .sp
 The list of supported formats and channel layouts can be retrieved using \fB\-\-list\-supported\fP option.
@@ -314,7 +311,7 @@ Where:
 .IP \(bu 2
 \fBid\fP is an arbitrary number in range 100..127, which should uniquely identify encoding on all related senders and receivers
 .IP \(bu 2
-\fBformat\fP defines sample precision and binary representation, e.g. \fBs16_le\fP stands for little\-endian signed 16\-bit integers
+\fBformat\fP defines sample precision and binary representation, e.g. \fBpcm_s24\fP stands for 24\-bit signed integer PCM
 .IP \(bu 2
 \fBrate\fP defines sample rate in Hertz (number of samples per second), e.g. \fB48000\fP
 .IP \(bu 2
@@ -324,9 +321,9 @@ Where:
 Examples:
 .INDENT 0.0
 .IP \(bu 2
-\fB101:s16_be/44100/mono\fP \-\- 16\-bit big\-endian integers, 44.1KHz, 1 channel
+\fB101:pcm_s24/44100/mono\fP \-\- 24\-bit network\-endian integers, 44.1KHz, 1 channel
 .IP \(bu 2
-\fB102:f32_le/48000/stereo\fP \-\- 32\-bit little\-endian floats, 48KHz, 2 channels
+\fB102:pcm_f32/48000/stereo\fP \-\- 32\-bit network\-endian floats, 48KHz, 2 channels
 .UNINDENT
 .sp
 The list of supported formats and channel layouts can be retrieved using \fB\-\-list\-supported\fP option.
@@ -609,7 +606,7 @@ Force specific encoding on the input device:
 .nf
 .ft C
 $ roc\-send \-vv \-s rtp://192.168.0.3:10001 \e
-    \-\-input alsa://hw:1,0 \-\-io\-encoding s32/48000/stereo
+    \-\-input alsa://hw:1,0 \-\-io\-encoding pcm_s32/48000/stereo
 .ft P
 .fi
 .UNINDENT
@@ -621,7 +618,7 @@ Use specific encoding for network packets:
 .sp
 .nf
 .ft C
-$ roc\-send \-vv \-s rtp://192.168.0.3:10001 \-\-packet\-encoding 101:s32/48000/stereo
+$ roc\-send \-vv \-s rtp://192.168.0.3:10001 \-\-packet\-encoding 101:pcm_s24/48000/stereo
 .ft P
 .fi
 .UNINDENT
@@ -631,7 +628,7 @@ $ roc\-send \-vv \-s rtp://192.168.0.3:10001 \-\-packet\-encoding 101:s32/48000/
 .sp
 .nf
 .ft C
-$ roc\-recv \-vv \-s rtp://0.0.0.0:10001 \-\-packet\-encoding 101:s32/48000/stereo
+$ roc\-recv \-vv \-s rtp://0.0.0.0:10001 \-\-packet\-encoding 101:pcm_s24/48000/stereo
 .ft P
 .fi
 .UNINDENT
diff --git a/docs/sphinx/manuals/roc_copy.rst b/docs/sphinx/manuals/roc_copy.rst
index eefdfba8b..fae7b21aa 100644
--- a/docs/sphinx/manuals/roc_copy.rst
+++ b/docs/sphinx/manuals/roc_copy.rst
@@ -32,9 +32,8 @@ I/O options
 -----------
 
 -i, --input=FILE_URI           Input file URI
---input-format=FILE_FORMAT     Force input file format
+--input-encoding=IO_ENCODING   Input file encoding
 -o, --output=FILE_URI          Output file URI
---output-format=FILE_FORMAT    Force output file format
 --output-encoding=IO_ENCODING  Output file encoding
 --io-frame-len=TIME            I/O frame length, TIME units
 
@@ -101,13 +100,13 @@ Convert sample rate to 24-bit 48k stereo:
 
 .. code::
 
-    $ roc-copy -vv --io-encoding s24/48000/stereo -i file:input.wav -o file:output.wav
+    $ roc-copy -vv --io-encoding pcm_s24/48000/stereo -i file:input.wav -o file:output.wav
 
 Same, but drop output results instead of writing to file (useful for benchmarking):
 
 .. code::
 
-    $ roc-copy -vv --io-encoding s24/48000/stereo -i file:input.wav
+    $ roc-copy -vv --io-encoding pcm_s24/48000/stereo -i file:input.wav
 
 Input from stdin, output to stdout:
 
diff --git a/docs/sphinx/manuals/roc_recv.rst b/docs/sphinx/manuals/roc_recv.rst
index e8adf53ca..42424ab23 100644
--- a/docs/sphinx/manuals/roc_recv.rst
+++ b/docs/sphinx/manuals/roc_recv.rst
@@ -36,17 +36,15 @@ Operation options
 Output options
 --------------
 
--o, --output=IO_URI          Output file or device URI
---output-format=FILE_FORMAT  Force output file format
---io-encoding=IO_ENCODING    Output device encoding
---io-latency=TIME            Output device latency, TIME units
---io-frame-len=TIME          Output frame length, TIME units
+-o, --output=IO_URI        Output file or device URI
+--io-encoding=IO_ENCODING  Output device encoding
+--io-latency=TIME          Output device latency, TIME units
+--io-frame-len=TIME        Output frame length, TIME units
 
 Backup input options
 --------------------
 
---backup=IO_URI              Backup file or device URI (used as input when there are no connections)
---backup-format=FILE_FORMAT  Force backup file format
+--backup=IO_URI  Backup file URI (used as input when there are no connections)
 
 Network options
 ---------------
@@ -150,7 +148,7 @@ This option is useful when device supports multiple encodings, or specific file
 
 Where:
 
-* ``format`` defines sample precision and binary representation, e.g. ``s16_le`` stands for little-endian signed 16-bit integers
+* ``format`` defines sample precision and binary representation, e.g. ``pcm_s16_le`` stands for little-endian signed 16-bit integer PCM
 * ``rate`` defines sample rate in Hertz (number of samples per second), e.g. ``48000``
 * ``channels`` defines channel layout, e.g. ``mono`` or ``stereo``
 
@@ -158,9 +156,9 @@ Any component may be set to special value ``-``, which means use default value f
 
 Examples:
 
-* ``s16/44100/mono`` -- 16-bit native-endian integers, 44.1KHz, 1 channel
-* ``f32_le/48000/stereo`` -- 32-bit little-endian floats, 48KHz, 2 channels
-* ``s24_4be/-/-`` -- 24-bit PCM packed into 4-byte big-endian frames, default rate and channels
+* ``pcm_s16/44100/mono`` -- 16-bit native-endian integers, 44.1KHz, 1 channel
+* ``pcm_f32_le/48000/stereo`` -- 32-bit little-endian floats, 48KHz, 2 channels
+* ``pcm_s24_4be/-/-`` -- 24-bit PCM packed into 4-byte big-endian frames, default rate and channels
 
 The list of supported formats and channel layouts can be retrieved using ``--list-supported`` option.
 
@@ -226,14 +224,14 @@ Packet encodings
 Where:
 
 * ``id`` is an arbitrary number in range 100..127, which should uniquely identify encoding on all related senders and receivers
-* ``format`` defines sample precision and binary representation, e.g. ``s16_le`` stands for little-endian signed 16-bit integers
+* ``format`` defines sample precision and binary representation, e.g. ``pcm_s24`` stands for 24-bit signed integer PCM
 * ``rate`` defines sample rate in Hertz (number of samples per second), e.g. ``48000``
 * ``channels`` defines channel layout, e.g. ``mono`` or ``stereo``
 
 Examples:
 
-* ``101:s16_be/44100/mono`` -- 16-bit big-endian integers, 44.1KHz, 1 channel
-* ``102:f32_le/48000/stereo`` -- 32-bit little-endian floats, 48KHz, 2 channels
+* ``101:pcm_s24/44100/mono`` -- 24-bit network-endian integers, 44.1KHz, 1 channel
+* ``102:pcm_f32/48000/stereo`` -- 32-bit network-endian floats, 48KHz, 2 channels
 
 The list of supported formats and channel layouts can be retrieved using ``--list-supported`` option.
 
@@ -457,17 +455,17 @@ Force specific encoding on the output device:
 .. code::
 
     $ roc-recv -vv -s rtp://0.0.0.0:10001 \
-        --output alsa://hw:1,0 --io-encoding s32/48000/stereo
+        --output alsa://hw:1,0 --io-encoding pcm_s32/48000/stereo
 
 Use specific encoding for network packets:
 
 .. code::
 
-    $ roc-send -vv -s rtp://192.168.0.3:10001 --packet-encoding 101:s32/48000/stereo
+    $ roc-send -vv -s rtp://192.168.0.3:10001 --packet-encoding 101:pcm_s24/48000/stereo
 
 .. code::
 
-    $ roc-recv -vv -s rtp://0.0.0.0:10001 --packet-encoding 101:s32/48000/stereo
+    $ roc-recv -vv -s rtp://0.0.0.0:10001 --packet-encoding 101:pcm_s24/48000/stereo
 
 Select the LDPC-Staircase FEC scheme:
 
diff --git a/docs/sphinx/manuals/roc_send.rst b/docs/sphinx/manuals/roc_send.rst
index 694769315..15324e3c7 100644
--- a/docs/sphinx/manuals/roc_send.rst
+++ b/docs/sphinx/manuals/roc_send.rst
@@ -31,11 +31,10 @@ General options
 Input options
 -------------
 
--i, --input=IO_URI          Input file or device URI
---input-format=FILE_FORMAT  Force input file format
---io-encoding=IO_ENCODING   Input device encoding
---io-latency=TIME           Input device latency, TIME units
---io-frame-len=TIME         Input frame length, TIME units
+-i, --input=IO_URI         Input file or device URI
+--io-encoding=IO_ENCODING  Input device encoding
+--io-latency=TIME          Input device latency, TIME units
+--io-frame-len=TIME        Input frame length, TIME units
 
 Network options
 ---------------
@@ -135,7 +134,7 @@ This option is useful when device supports multiple encodings. Note that I/O enc
 
 Where:
 
-* ``format`` defines sample precision and binary representation, e.g. ``s16_le`` stands for little-endian signed 16-bit integers
+* ``format`` defines sample precision and binary representation, e.g. ``pcm_s16_le`` stands for little-endian signed 16-bit integer PCM
 * ``rate`` defines sample rate in Hertz (number of samples per second), e.g. ``48000``
 * ``channels`` defines channel layout, e.g. ``mono`` or ``stereo``
 
@@ -143,9 +142,9 @@ Any component may be set to special value ``-``, which means use default value f
 
 Examples:
 
-* ``s16/44100/mono`` -- 16-bit native-endian integers, 44.1KHz, 1 channel
-* ``f32_le/48000/stereo`` -- 32-bit little-endian floats, 48KHz, 2 channels
-* ``s24_4be/-/-`` -- 24-bit PCM packed into 4-byte big-endian frames, default rate and channels
+* ``pcm_s16/44100/mono`` -- 16-bit native-endian integers, 44.1KHz, 1 channel
+* ``pcm_f32_le/48000/stereo`` -- 32-bit little-endian floats, 48KHz, 2 channels
+* ``pcm_s24_4be/-/-`` -- 24-bit PCM packed into 4-byte big-endian frames, default rate and channels
 
 The list of supported formats and channel layouts can be retrieved using ``--list-supported`` option.
 
@@ -211,14 +210,14 @@ Packet encoding
 Where:
 
 * ``id`` is an arbitrary number in range 100..127, which should uniquely identify encoding on all related senders and receivers
-* ``format`` defines sample precision and binary representation, e.g. ``s16_le`` stands for little-endian signed 16-bit integers
+* ``format`` defines sample precision and binary representation, e.g. ``pcm_s24`` stands for 24-bit signed integer PCM
 * ``rate`` defines sample rate in Hertz (number of samples per second), e.g. ``48000``
 * ``channels`` defines channel layout, e.g. ``mono`` or ``stereo``
 
 Examples:
 
-* ``101:s16_be/44100/mono`` -- 16-bit big-endian integers, 44.1KHz, 1 channel
-* ``102:f32_le/48000/stereo`` -- 32-bit little-endian floats, 48KHz, 2 channels
+* ``101:pcm_s24/44100/mono`` -- 24-bit network-endian integers, 44.1KHz, 1 channel
+* ``102:pcm_f32/48000/stereo`` -- 32-bit network-endian floats, 48KHz, 2 channels
 
 The list of supported formats and channel layouts can be retrieved using ``--list-supported`` option.
 
@@ -432,17 +431,17 @@ Force specific encoding on the input device:
 .. code::
 
     $ roc-send -vv -s rtp://192.168.0.3:10001 \
-        --input alsa://hw:1,0 --io-encoding s32/48000/stereo
+        --input alsa://hw:1,0 --io-encoding pcm_s32/48000/stereo
 
 Use specific encoding for network packets:
 
 .. code::
 
-    $ roc-send -vv -s rtp://192.168.0.3:10001 --packet-encoding 101:s32/48000/stereo
+    $ roc-send -vv -s rtp://192.168.0.3:10001 --packet-encoding 101:pcm_s24/48000/stereo
 
 .. code::
 
-    $ roc-recv -vv -s rtp://0.0.0.0:10001 --packet-encoding 101:s32/48000/stereo
+    $ roc-recv -vv -s rtp://0.0.0.0:10001 --packet-encoding 101:pcm_s24/48000/stereo
 
 Select the LDPC-Staircase FEC scheme and a larger block size:
 
diff --git a/src/internal_modules/roc_audio/beep_plc.cpp b/src/internal_modules/roc_audio/beep_plc.cpp
index 3a64b13d5..b159a4776 100644
--- a/src/internal_modules/roc_audio/beep_plc.cpp
+++ b/src/internal_modules/roc_audio/beep_plc.cpp
@@ -20,8 +20,8 @@ BeepPlc::BeepPlc(const PlcConfig& config,
     : IPlc(arena)
     , sample_spec_(sample_spec)
     , signal_pos_(0) {
-    if (!sample_spec_.is_valid() || !sample_spec_.is_raw()) {
-        roc_panic("beep plc: required valid sample specs with raw format: spec=%s",
+    if (!sample_spec_.is_complete() || !sample_spec_.is_raw()) {
+        roc_panic("beep plc: required complete sample specs with raw format: spec=%s",
                   sample_spec_to_str(sample_spec_).c_str());
     }
 }
diff --git a/src/internal_modules/roc_audio/builtin_resampler.cpp b/src/internal_modules/roc_audio/builtin_resampler.cpp
index 4dadf4627..142ddfdb7 100644
--- a/src/internal_modules/roc_audio/builtin_resampler.cpp
+++ b/src/internal_modules/roc_audio/builtin_resampler.cpp
@@ -145,9 +145,9 @@ BuiltinResampler::BuiltinResampler(const ResamplerConfig& config,
     , qt_dt_(0)
     , cutoff_freq_(0.9f)
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid() || !in_spec_.is_raw()
+    if (!in_spec_.is_complete() || !out_spec_.is_complete() || !in_spec_.is_raw()
         || !out_spec_.is_raw()) {
-        roc_panic("builtin resampler: required valid sample specs with raw format:"
+        roc_panic("builtin resampler: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
diff --git a/src/internal_modules/roc_audio/channel_mapper_reader.cpp b/src/internal_modules/roc_audio/channel_mapper_reader.cpp
index 80db79be2..f54196aa2 100644
--- a/src/internal_modules/roc_audio/channel_mapper_reader.cpp
+++ b/src/internal_modules/roc_audio/channel_mapper_reader.cpp
@@ -25,9 +25,9 @@ ChannelMapperReader::ChannelMapperReader(IFrameReader& frame_reader,
     , in_spec_(in_spec)
     , out_spec_(out_spec)
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid() || !in_spec_.is_raw()
+    if (!in_spec_.is_complete() || !out_spec_.is_complete() || !in_spec_.is_raw()
         || !out_spec_.is_raw()) {
-        roc_panic("channel mapper reader: required valid sample specs with raw format:"
+        roc_panic("channel mapper reader: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
diff --git a/src/internal_modules/roc_audio/channel_mapper_writer.cpp b/src/internal_modules/roc_audio/channel_mapper_writer.cpp
index 27dc94464..a9eb57c77 100644
--- a/src/internal_modules/roc_audio/channel_mapper_writer.cpp
+++ b/src/internal_modules/roc_audio/channel_mapper_writer.cpp
@@ -25,9 +25,9 @@ ChannelMapperWriter::ChannelMapperWriter(IFrameWriter& frame_writer,
     , in_spec_(in_spec)
     , out_spec_(out_spec)
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid() || !in_spec_.is_raw()
+    if (!in_spec_.is_complete() || !out_spec_.is_complete() || !in_spec_.is_raw()
         || !out_spec_.is_raw()) {
-        roc_panic("channel mapper writer: required valid sample specs with raw format:"
+        roc_panic("channel mapper writer: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
diff --git a/src/internal_modules/roc_audio/channel_set_format.cpp b/src/internal_modules/roc_audio/channel_set_format.cpp
index 02b2e9ec2..2c0efda1c 100644
--- a/src/internal_modules/roc_audio/channel_set_format.cpp
+++ b/src/internal_modules/roc_audio/channel_set_format.cpp
@@ -20,46 +20,46 @@ void format_channel_set(const ChannelSet& ch_set, core::StringBuilder& bld) {
         bld.append_str(channel_order_to_str(ch_set.order()));
     }
 
-    bld.append_str(" n_ch=");
+    bld.append_str(" ");
     bld.append_uint(ch_set.num_channels(), 10);
 
-    if (ch_set.num_channels() != 0) {
-        if (ch_set.layout() == ChanLayout_Surround) {
-            bld.append_str(" ch=");
+    if (ch_set.num_channels() == 0) {
+        bld.append_str(" none");
+    } else if (ch_set.layout() == ChanLayout_Surround) {
+        bld.append_str(" ");
 
-            for (size_t ch = ch_set.first_channel(); ch <= ch_set.last_channel(); ch++) {
-                if (!ch_set.has_channel(ch)) {
-                    continue;
-                }
-                if (ch != ch_set.first_channel()) {
-                    bld.append_str(",");
-                }
-                bld.append_str(channel_pos_to_str((ChannelPosition)ch));
+        for (size_t ch = ch_set.first_channel(); ch <= ch_set.last_channel(); ch++) {
+            if (!ch_set.has_channel(ch)) {
+                continue;
+            }
+            if (ch != ch_set.first_channel()) {
+                bld.append_str(",");
             }
-        } else {
-            bld.append_str(" ch=0x");
+            bld.append_str(channel_pos_to_str((ChannelPosition)ch));
+        }
+    } else {
+        bld.append_str(" 0x");
 
-            size_t last_byte = 0;
+        size_t last_byte = 0;
 
-            for (size_t n = 0; n < ch_set.num_bytes(); n++) {
-                if (ch_set.byte_at(n) != 0) {
-                    last_byte = n;
-                }
+        for (size_t n = 0; n < ch_set.num_bytes(); n++) {
+            if (ch_set.byte_at(n) != 0) {
+                last_byte = n;
             }
+        }
 
-            size_t n = last_byte;
-            do {
-                const uint8_t byte = ch_set.byte_at(n);
+        size_t n = last_byte;
+        do {
+            const uint8_t byte = ch_set.byte_at(n);
 
-                const uint8_t lo = (byte & 0xf);
-                const uint8_t hi = ((byte >> 4) & 0xf);
+            const uint8_t lo = (byte & 0xf);
+            const uint8_t hi = ((byte >> 4) & 0xf);
 
-                if (hi != 0 || n != last_byte) {
-                    bld.append_uint(hi, 16);
-                }
-                bld.append_uint(lo, 16);
-            } while (n-- != 0);
-        }
+            if (hi != 0 || n != last_byte) {
+                bld.append_uint(hi, 16);
+            }
+            bld.append_uint(lo, 16);
+        } while (n-- != 0);
     }
 
     bld.append_str(">");
diff --git a/src/internal_modules/roc_audio/decimation_resampler.cpp b/src/internal_modules/roc_audio/decimation_resampler.cpp
index cec39f90d..e2e1ec175 100644
--- a/src/internal_modules/roc_audio/decimation_resampler.cpp
+++ b/src/internal_modules/roc_audio/decimation_resampler.cpp
@@ -45,9 +45,9 @@ DecimationResampler::DecimationResampler(
     , decim_count_(0)
     , report_limiter_(LogReportInterval)
     , init_status_(status::NoStatus) {
-    if (!in_spec.is_valid() || !out_spec.is_valid() || !in_spec.is_raw()
+    if (!in_spec.is_complete() || !out_spec.is_complete() || !in_spec.is_raw()
         || !out_spec.is_raw()) {
-        roc_panic("decimation resampler: required valid sample specs with raw format:"
+        roc_panic("decimation resampler: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec).c_str(),
                   sample_spec_to_str(out_spec).c_str());
diff --git a/src/internal_modules/roc_audio/depacketizer.cpp b/src/internal_modules/roc_audio/depacketizer.cpp
index f0815230e..e05766c11 100644
--- a/src/internal_modules/roc_audio/depacketizer.cpp
+++ b/src/internal_modules/roc_audio/depacketizer.cpp
@@ -42,8 +42,8 @@ Depacketizer::Depacketizer(packet::IReader& packet_reader,
     , rate_limiter_(LogInterval)
     , dumper_(dumper)
     , init_status_(status::NoStatus) {
-    roc_panic_if_msg(!sample_spec_.is_valid() || !sample_spec_.is_raw(),
-                     "depacketizer: required valid sample spec with raw format: %s",
+    roc_panic_if_msg(!sample_spec_.is_complete() || !sample_spec_.is_raw(),
+                     "depacketizer: required complete sample spec with raw format: %s",
                      sample_spec_to_str(sample_spec_).c_str());
 
     roc_log(LogDebug, "depacketizer: initializing: sample_rate=%lu n_channels=%lu",
diff --git a/src/internal_modules/roc_audio/fanout.cpp b/src/internal_modules/roc_audio/fanout.cpp
index 0c67a02dd..3c4a715a6 100644
--- a/src/internal_modules/roc_audio/fanout.cpp
+++ b/src/internal_modules/roc_audio/fanout.cpp
@@ -20,7 +20,8 @@ Fanout::Fanout(const SampleSpec& sample_spec,
     : outputs_(arena)
     , sample_spec_(sample_spec)
     , init_status_(status::NoStatus) {
-    roc_panic_if_msg(!sample_spec_.is_valid(), "fanout: required valid sample spec: %s",
+    roc_panic_if_msg(!sample_spec_.is_complete(),
+                     "fanout: required complete sample spec: %s",
                      sample_spec_to_str(sample_spec_).c_str());
 
     init_status_ = status::StatusOK;
diff --git a/src/internal_modules/roc_audio/format.cpp b/src/internal_modules/roc_audio/format.cpp
new file mode 100644
index 000000000..78bb8f93a
--- /dev/null
+++ b/src/internal_modules/roc_audio/format.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2023 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include "roc_audio/format.h"
+#include "roc_core/macro_helpers.h"
+#include "roc_core/stddefs.h"
+
+namespace roc {
+namespace audio {
+
+namespace {
+
+const FormatTraits formats[] = {
+    { Format_Pcm, "pcm",
+      Format_SupportsNetwork | Format_SupportsDevices | Format_SupportsFiles },
+    { Format_Wav, "wav", Format_SupportsFiles },
+};
+
+} // namespace
+
+FormatTraits format_traits(Format format) {
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(formats); n++) {
+        if (formats[n].id == format) {
+            return formats[n];
+        }
+    }
+
+    FormatTraits ret;
+    memset(&ret, 0, sizeof(ret));
+    ret.id = Format_Invalid;
+    return ret;
+}
+
+const char* format_to_str(Format format) {
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(formats); n++) {
+        if (formats[n].id == format) {
+            return formats[n].name;
+        }
+    }
+
+    return "invalid";
+}
+
+Format format_from_str(const char* str) {
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(formats); n++) {
+        if (strcmp(formats[n].name, str) == 0) {
+            return formats[n].id;
+        }
+    }
+
+    return Format_Invalid;
+}
+
+} // namespace audio
+} // namespace roc
diff --git a/src/internal_modules/roc_audio/format.h b/src/internal_modules/roc_audio/format.h
new file mode 100644
index 000000000..ea5ce6a6c
--- /dev/null
+++ b/src/internal_modules/roc_audio/format.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+//! @file roc_audio/format.h
+//! @brief Audio format.
+
+#ifndef ROC_AUDIO_FORMAT_H_
+#define ROC_AUDIO_FORMAT_H_
+
+#include "roc_audio/pcm_subformat.h"
+
+namespace roc {
+namespace audio {
+
+//! Audio format.
+//! Defines representation of samples in memory.
+//! Does not define sample depth, rate and channel set.
+enum Format {
+    //! Invalid format.
+    Format_Invalid,
+
+    //! Interleaved PCM format.
+    //! @note
+    //!  Can be used for network packets, devices, files.
+    //! @note
+    //!  This format requires sub-format of type PcmSubformat, which defines
+    //!  sample type, width, and endian.
+    Format_Pcm,
+
+    //! WAV file.
+    //! @note
+    //!  Can be used for files.
+    //! @note
+    //!  This format allows optional sub-format of type PcmSubformat, which defines
+    //!  sample type, width, and endian. However, not every PCM sub-format is
+    //!  supported. If sub-format is omitted, default sub-format is used.
+    Format_Wav,
+
+    //! Custom opaque format.
+    //! @remarks
+    //!  Used to specify custom format for file or device via its string
+    //!  name, when we don't have and don't need enum value for it.
+    Format_Custom,
+
+    //! Maximum enum value.
+    Format_Max
+};
+
+//! Audio format flags.
+enum FormatFlags {
+    //! Format can be used for network packets.
+    Format_SupportsNetwork = (1 << 0),
+
+    //! Format can be used for audio devices.
+    Format_SupportsDevices = (1 << 1),
+
+    //! Format can be used for audio files.
+    Format_SupportsFiles = (1 << 2)
+};
+
+//! Audio format meta-information.
+struct FormatTraits {
+    //! Numeric identifier.
+    Format id;
+
+    //! String name.
+    const char* name;
+
+    //! Flags.
+    unsigned flags;
+
+    //! Check if all given flags are set.
+    bool has_flags(unsigned mask) const {
+        return (flags & mask) == mask;
+    }
+};
+
+//! Get format traits.
+FormatTraits format_traits(Format format);
+
+//! Get string name of audio format.
+const char* format_to_str(Format format);
+
+//! Get audio format from string name.
+Format format_from_str(const char* str);
+
+} // namespace audio
+} // namespace roc
+
+#endif // ROC_AUDIO_FORMAT_H_
diff --git a/src/internal_modules/roc_audio/mixer.cpp b/src/internal_modules/roc_audio/mixer.cpp
index b86506f26..c6c0531c2 100644
--- a/src/internal_modules/roc_audio/mixer.cpp
+++ b/src/internal_modules/roc_audio/mixer.cpp
@@ -23,8 +23,8 @@ Mixer::Mixer(const SampleSpec& sample_spec,
     , sample_spec_(sample_spec)
     , enable_timestamps_(enable_timestamps)
     , init_status_(status::NoStatus) {
-    roc_panic_if_msg(!sample_spec_.is_valid() || !sample_spec_.is_raw(),
-                     "mixer: required valid sample spec with raw format: %s",
+    roc_panic_if_msg(!sample_spec_.is_complete() || !sample_spec_.is_raw(),
+                     "mixer: required complete sample spec with raw format: %s",
                      sample_spec_to_str(sample_spec_).c_str());
 
     in_frame_ = frame_factory_.allocate_frame(0);
diff --git a/src/internal_modules/roc_audio/packetizer.cpp b/src/internal_modules/roc_audio/packetizer.cpp
index 3a1083562..82618b85f 100644
--- a/src/internal_modules/roc_audio/packetizer.cpp
+++ b/src/internal_modules/roc_audio/packetizer.cpp
@@ -34,8 +34,8 @@ Packetizer::Packetizer(packet::IWriter& writer,
     , packet_cts_(0)
     , capture_ts_(0)
     , init_status_(status::NoStatus) {
-    roc_panic_if_msg(!sample_spec_.is_valid() || !sample_spec_.is_raw(),
-                     "packetizer: required valid sample spec with raw format: %s",
+    roc_panic_if_msg(!sample_spec_.is_complete() || !sample_spec_.is_raw(),
+                     "packetizer: required complete sample spec with raw format: %s",
                      sample_spec_to_str(sample_spec_).c_str());
 
     if (packet_length <= 0 || sample_spec.ns_2_stream_timestamp(packet_length) <= 0) {
diff --git a/src/internal_modules/roc_audio/pcm_decoder.cpp b/src/internal_modules/roc_audio/pcm_decoder.cpp
index 96f1c732d..4d233b3bd 100644
--- a/src/internal_modules/roc_audio/pcm_decoder.cpp
+++ b/src/internal_modules/roc_audio/pcm_decoder.cpp
@@ -19,7 +19,7 @@ IFrameDecoder* PcmDecoder::construct(const SampleSpec& sample_spec, core::IArena
 
 PcmDecoder::PcmDecoder(const SampleSpec& sample_spec, core::IArena& arena)
     : IFrameDecoder(arena)
-    , pcm_mapper_(sample_spec.pcm_format(), Sample_RawFormat)
+    , pcm_mapper_(sample_spec.pcm_subformat(), PcmSubformat_Raw)
     , n_chans_(sample_spec.num_channels())
     , stream_pos_(0)
     , stream_avail_(0)
diff --git a/src/internal_modules/roc_audio/pcm_encoder.cpp b/src/internal_modules/roc_audio/pcm_encoder.cpp
index 49ef31433..020da933c 100644
--- a/src/internal_modules/roc_audio/pcm_encoder.cpp
+++ b/src/internal_modules/roc_audio/pcm_encoder.cpp
@@ -18,7 +18,7 @@ IFrameEncoder* PcmEncoder::construct(const SampleSpec& sample_spec, core::IArena
 
 PcmEncoder::PcmEncoder(const SampleSpec& sample_spec, core::IArena& arena)
     : IFrameEncoder(arena)
-    , pcm_mapper_(Sample_RawFormat, sample_spec.pcm_format())
+    , pcm_mapper_(PcmSubformat_Raw, sample_spec.pcm_subformat())
     , n_chans_(sample_spec.num_channels())
     , frame_data_(NULL)
     , frame_byte_size_(0)
diff --git a/src/internal_modules/roc_audio/pcm_format.cpp b/src/internal_modules/roc_audio/pcm_format.cpp
index 175fb2670..e8dc14996 100644
--- a/src/internal_modules/roc_audio/pcm_format.cpp
+++ b/src/internal_modules/roc_audio/pcm_format.cpp
@@ -1,8 +1,9 @@
 /*
- * THIS FILE IS AUTO-GENERATED USING `pcm_format_gen.py'. DO NOT EDIT!
+ * THIS FILE IS AUTO-GENERATED USING `pcm_subformat_gen.py'. DO NOT EDIT!
  */
 
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
+#include "roc_audio/pcm_subformat_rw.h"
 #include "roc_core/attributes.h"
 #include "roc_core/cpu_traits.h"
 #include "roc_core/stddefs.h"
@@ -2151,7 +2152,6 @@ template <> struct pcm_code_converter<PcmCode_Float64, PcmCode_Float64> {
     }
 };
 
-
 // N-byte native-endian sample
 template <class T> struct pcm_sample;
 
@@ -2359,54 +2359,6 @@ template <> struct pcm_sample<double> {
     };
 };
 
-// Write octet at given byte-aligned bit offset
-inline void pcm_aligned_write(uint8_t* buffer, size_t& bit_offset, uint8_t arg) {
-    buffer[bit_offset >> 3] = arg;
-    bit_offset += 8;
-}
-
-// Read octet at given byte-aligned bit offset
-inline uint8_t pcm_aligned_read(const uint8_t* buffer, size_t& bit_offset) {
-    uint8_t ret = buffer[bit_offset >> 3];
-    bit_offset += 8;
-    return ret;
-}
-
-// Write value (at most 8 bits) at given unaligned bit offset
-inline void
-pcm_unaligned_write(uint8_t* buffer, size_t& bit_offset, size_t bit_length, uint8_t arg) {
-    size_t byte_index = (bit_offset >> 3);
-    size_t bit_index = (bit_offset & 0x7u);
-
-    if (bit_index == 0) {
-        buffer[byte_index] = 0;
-    }
-
-    buffer[byte_index] |= uint8_t(uint8_t(arg << (8 - bit_length)) >> bit_index);
-
-    if (bit_index + bit_length > 8) {
-        buffer[byte_index + 1] = uint8_t(arg << bit_index);
-    }
-
-    bit_offset += bit_length;
-}
-
-// Read value (at most 8 bits) at given unaligned bit offset
-inline uint8_t
-pcm_unaligned_read(const uint8_t* buffer, size_t& bit_offset, size_t bit_length) {
-    size_t byte_index = (bit_offset >> 3);
-    size_t bit_index = (bit_offset & 0x7u);
-
-    uint8_t ret = uint8_t(uint8_t(buffer[byte_index] << bit_index) >> (8 - bit_length));
-
-    if (bit_index + bit_length > 8) {
-        ret |= uint8_t(buffer[byte_index + 1] >> ((8 - bit_index) + (8 - bit_length)));
-    }
-
-    bit_offset += bit_length;
-    return ret;
-}
-
 // Sample packer / unpacker
 template <PcmCode, PcmEndian> struct pcm_packer;
 
@@ -4177,14 +4129,14 @@ struct pcm_mapper {
 
 // Select mapping function
 template <PcmCode InCode, PcmEndian InEndian>
-PcmMapFn pcm_map_to_raw(PcmFormat raw_format) {
+PcmMapFn pcm_map_to_raw(PcmSubformat raw_format) {
     switch (raw_format) {
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
-    case PcmFormat_Float32:
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32:
+    case PcmSubformat_Float32_Be:
 #else
-    case PcmFormat_Float32:
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32:
+    case PcmSubformat_Float32_Le:
 #endif
         return &pcm_mapper<InCode, InEndian, PcmCode_Float32, PcmEndian_Default>::map;
     default:
@@ -4195,14 +4147,14 @@ PcmMapFn pcm_map_to_raw(PcmFormat raw_format) {
 
 // Select mapping function
 template <PcmCode OutCode, PcmEndian OutEndian>
-PcmMapFn pcm_map_from_raw(PcmFormat raw_format) {
+PcmMapFn pcm_map_from_raw(PcmSubformat raw_format) {
     switch (raw_format) {
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
-    case PcmFormat_Float32:
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32:
+    case PcmSubformat_Float32_Be:
 #else
-    case PcmFormat_Float32:
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32:
+    case PcmSubformat_Float32_Le:
 #endif
         return &pcm_mapper<PcmCode_Float32, PcmEndian_Default, OutCode, OutEndian>::map;
     default:
@@ -4214,165 +4166,165 @@ PcmMapFn pcm_map_from_raw(PcmFormat raw_format) {
 } // namespace
 
 // Select mapping function
-PcmMapFn pcm_format_mapfn(PcmFormat in_format, PcmFormat out_format) {
+PcmMapFn pcm_subformat_mapfn(PcmSubformat in_format, PcmSubformat out_format) {
     // non-raw to raw
     switch (in_format) {
-    case PcmFormat_SInt8:
+    case PcmSubformat_SInt8:
         return pcm_map_to_raw<PcmCode_SInt8, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt8_Be:
+    case PcmSubformat_SInt8_Be:
         return pcm_map_to_raw<PcmCode_SInt8, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt8_Le:
+    case PcmSubformat_SInt8_Le:
         return pcm_map_to_raw<PcmCode_SInt8, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt8:
+    case PcmSubformat_UInt8:
         return pcm_map_to_raw<PcmCode_UInt8, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt8_Be:
+    case PcmSubformat_UInt8_Be:
         return pcm_map_to_raw<PcmCode_UInt8, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt8_Le:
+    case PcmSubformat_UInt8_Le:
         return pcm_map_to_raw<PcmCode_UInt8, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt16:
+    case PcmSubformat_SInt16:
         return pcm_map_to_raw<PcmCode_SInt16, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt16_Be:
+    case PcmSubformat_SInt16_Be:
         return pcm_map_to_raw<PcmCode_SInt16, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt16_Le:
+    case PcmSubformat_SInt16_Le:
         return pcm_map_to_raw<PcmCode_SInt16, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt16:
+    case PcmSubformat_UInt16:
         return pcm_map_to_raw<PcmCode_UInt16, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt16_Be:
+    case PcmSubformat_UInt16_Be:
         return pcm_map_to_raw<PcmCode_UInt16, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt16_Le:
+    case PcmSubformat_UInt16_Le:
         return pcm_map_to_raw<PcmCode_UInt16, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt18:
+    case PcmSubformat_SInt18:
         return pcm_map_to_raw<PcmCode_SInt18, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt18_Be:
+    case PcmSubformat_SInt18_Be:
         return pcm_map_to_raw<PcmCode_SInt18, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt18_Le:
+    case PcmSubformat_SInt18_Le:
         return pcm_map_to_raw<PcmCode_SInt18, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt18:
+    case PcmSubformat_UInt18:
         return pcm_map_to_raw<PcmCode_UInt18, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt18_Be:
+    case PcmSubformat_UInt18_Be:
         return pcm_map_to_raw<PcmCode_UInt18, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt18_Le:
+    case PcmSubformat_UInt18_Le:
         return pcm_map_to_raw<PcmCode_UInt18, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt18_3:
+    case PcmSubformat_SInt18_3:
         return pcm_map_to_raw<PcmCode_SInt18_3, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt18_3_Be:
+    case PcmSubformat_SInt18_3_Be:
         return pcm_map_to_raw<PcmCode_SInt18_3, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt18_3_Le:
+    case PcmSubformat_SInt18_3_Le:
         return pcm_map_to_raw<PcmCode_SInt18_3, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt18_3:
+    case PcmSubformat_UInt18_3:
         return pcm_map_to_raw<PcmCode_UInt18_3, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt18_3_Be:
+    case PcmSubformat_UInt18_3_Be:
         return pcm_map_to_raw<PcmCode_UInt18_3, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt18_3_Le:
+    case PcmSubformat_UInt18_3_Le:
         return pcm_map_to_raw<PcmCode_UInt18_3, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt18_4:
+    case PcmSubformat_SInt18_4:
         return pcm_map_to_raw<PcmCode_SInt18_4, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt18_4_Be:
+    case PcmSubformat_SInt18_4_Be:
         return pcm_map_to_raw<PcmCode_SInt18_4, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt18_4_Le:
+    case PcmSubformat_SInt18_4_Le:
         return pcm_map_to_raw<PcmCode_SInt18_4, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt18_4:
+    case PcmSubformat_UInt18_4:
         return pcm_map_to_raw<PcmCode_UInt18_4, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt18_4_Be:
+    case PcmSubformat_UInt18_4_Be:
         return pcm_map_to_raw<PcmCode_UInt18_4, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt18_4_Le:
+    case PcmSubformat_UInt18_4_Le:
         return pcm_map_to_raw<PcmCode_UInt18_4, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt20:
+    case PcmSubformat_SInt20:
         return pcm_map_to_raw<PcmCode_SInt20, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt20_Be:
+    case PcmSubformat_SInt20_Be:
         return pcm_map_to_raw<PcmCode_SInt20, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt20_Le:
+    case PcmSubformat_SInt20_Le:
         return pcm_map_to_raw<PcmCode_SInt20, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt20:
+    case PcmSubformat_UInt20:
         return pcm_map_to_raw<PcmCode_UInt20, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt20_Be:
+    case PcmSubformat_UInt20_Be:
         return pcm_map_to_raw<PcmCode_UInt20, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt20_Le:
+    case PcmSubformat_UInt20_Le:
         return pcm_map_to_raw<PcmCode_UInt20, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt20_3:
+    case PcmSubformat_SInt20_3:
         return pcm_map_to_raw<PcmCode_SInt20_3, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt20_3_Be:
+    case PcmSubformat_SInt20_3_Be:
         return pcm_map_to_raw<PcmCode_SInt20_3, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt20_3_Le:
+    case PcmSubformat_SInt20_3_Le:
         return pcm_map_to_raw<PcmCode_SInt20_3, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt20_3:
+    case PcmSubformat_UInt20_3:
         return pcm_map_to_raw<PcmCode_UInt20_3, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt20_3_Be:
+    case PcmSubformat_UInt20_3_Be:
         return pcm_map_to_raw<PcmCode_UInt20_3, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt20_3_Le:
+    case PcmSubformat_UInt20_3_Le:
         return pcm_map_to_raw<PcmCode_UInt20_3, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt20_4:
+    case PcmSubformat_SInt20_4:
         return pcm_map_to_raw<PcmCode_SInt20_4, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt20_4_Be:
+    case PcmSubformat_SInt20_4_Be:
         return pcm_map_to_raw<PcmCode_SInt20_4, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt20_4_Le:
+    case PcmSubformat_SInt20_4_Le:
         return pcm_map_to_raw<PcmCode_SInt20_4, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt20_4:
+    case PcmSubformat_UInt20_4:
         return pcm_map_to_raw<PcmCode_UInt20_4, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt20_4_Be:
+    case PcmSubformat_UInt20_4_Be:
         return pcm_map_to_raw<PcmCode_UInt20_4, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt20_4_Le:
+    case PcmSubformat_UInt20_4_Le:
         return pcm_map_to_raw<PcmCode_UInt20_4, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt24:
+    case PcmSubformat_SInt24:
         return pcm_map_to_raw<PcmCode_SInt24, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt24_Be:
+    case PcmSubformat_SInt24_Be:
         return pcm_map_to_raw<PcmCode_SInt24, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt24_Le:
+    case PcmSubformat_SInt24_Le:
         return pcm_map_to_raw<PcmCode_SInt24, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt24:
+    case PcmSubformat_UInt24:
         return pcm_map_to_raw<PcmCode_UInt24, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt24_Be:
+    case PcmSubformat_UInt24_Be:
         return pcm_map_to_raw<PcmCode_UInt24, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt24_Le:
+    case PcmSubformat_UInt24_Le:
         return pcm_map_to_raw<PcmCode_UInt24, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt24_4:
+    case PcmSubformat_SInt24_4:
         return pcm_map_to_raw<PcmCode_SInt24_4, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt24_4_Be:
+    case PcmSubformat_SInt24_4_Be:
         return pcm_map_to_raw<PcmCode_SInt24_4, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt24_4_Le:
+    case PcmSubformat_SInt24_4_Le:
         return pcm_map_to_raw<PcmCode_SInt24_4, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt24_4:
+    case PcmSubformat_UInt24_4:
         return pcm_map_to_raw<PcmCode_UInt24_4, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt24_4_Be:
+    case PcmSubformat_UInt24_4_Be:
         return pcm_map_to_raw<PcmCode_UInt24_4, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt24_4_Le:
+    case PcmSubformat_UInt24_4_Le:
         return pcm_map_to_raw<PcmCode_UInt24_4, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt32:
+    case PcmSubformat_SInt32:
         return pcm_map_to_raw<PcmCode_SInt32, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt32_Be:
+    case PcmSubformat_SInt32_Be:
         return pcm_map_to_raw<PcmCode_SInt32, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt32_Le:
+    case PcmSubformat_SInt32_Le:
         return pcm_map_to_raw<PcmCode_SInt32, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt32:
+    case PcmSubformat_UInt32:
         return pcm_map_to_raw<PcmCode_UInt32, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt32_Be:
+    case PcmSubformat_UInt32_Be:
         return pcm_map_to_raw<PcmCode_UInt32, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt32_Le:
+    case PcmSubformat_UInt32_Le:
         return pcm_map_to_raw<PcmCode_UInt32, PcmEndian_Little>(out_format);
-    case PcmFormat_SInt64:
+    case PcmSubformat_SInt64:
         return pcm_map_to_raw<PcmCode_SInt64, PcmEndian_Default>(out_format);
-    case PcmFormat_SInt64_Be:
+    case PcmSubformat_SInt64_Be:
         return pcm_map_to_raw<PcmCode_SInt64, PcmEndian_Big>(out_format);
-    case PcmFormat_SInt64_Le:
+    case PcmSubformat_SInt64_Le:
         return pcm_map_to_raw<PcmCode_SInt64, PcmEndian_Little>(out_format);
-    case PcmFormat_UInt64:
+    case PcmSubformat_UInt64:
         return pcm_map_to_raw<PcmCode_UInt64, PcmEndian_Default>(out_format);
-    case PcmFormat_UInt64_Be:
+    case PcmSubformat_UInt64_Be:
         return pcm_map_to_raw<PcmCode_UInt64, PcmEndian_Big>(out_format);
-    case PcmFormat_UInt64_Le:
+    case PcmSubformat_UInt64_Le:
         return pcm_map_to_raw<PcmCode_UInt64, PcmEndian_Little>(out_format);
 #if ROC_CPU_ENDIAN != ROC_CPU_BE
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32_Be:
         return pcm_map_to_raw<PcmCode_Float32, PcmEndian_Big>(out_format);
 #else
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32_Le:
         return pcm_map_to_raw<PcmCode_Float32, PcmEndian_Little>(out_format);
 #endif
-    case PcmFormat_Float64:
+    case PcmSubformat_Float64:
         return pcm_map_to_raw<PcmCode_Float64, PcmEndian_Default>(out_format);
-    case PcmFormat_Float64_Be:
+    case PcmSubformat_Float64_Be:
         return pcm_map_to_raw<PcmCode_Float64, PcmEndian_Big>(out_format);
-    case PcmFormat_Float64_Le:
+    case PcmSubformat_Float64_Le:
         return pcm_map_to_raw<PcmCode_Float64, PcmEndian_Little>(out_format);
     default:
         break;
@@ -4380,162 +4332,162 @@ PcmMapFn pcm_format_mapfn(PcmFormat in_format, PcmFormat out_format) {
 
     // raw to non-raw
     switch (out_format) {
-    case PcmFormat_SInt8:
+    case PcmSubformat_SInt8:
         return pcm_map_from_raw<PcmCode_SInt8, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt8_Be:
+    case PcmSubformat_SInt8_Be:
         return pcm_map_from_raw<PcmCode_SInt8, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt8_Le:
+    case PcmSubformat_SInt8_Le:
         return pcm_map_from_raw<PcmCode_SInt8, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt8:
+    case PcmSubformat_UInt8:
         return pcm_map_from_raw<PcmCode_UInt8, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt8_Be:
+    case PcmSubformat_UInt8_Be:
         return pcm_map_from_raw<PcmCode_UInt8, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt8_Le:
+    case PcmSubformat_UInt8_Le:
         return pcm_map_from_raw<PcmCode_UInt8, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt16:
+    case PcmSubformat_SInt16:
         return pcm_map_from_raw<PcmCode_SInt16, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt16_Be:
+    case PcmSubformat_SInt16_Be:
         return pcm_map_from_raw<PcmCode_SInt16, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt16_Le:
+    case PcmSubformat_SInt16_Le:
         return pcm_map_from_raw<PcmCode_SInt16, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt16:
+    case PcmSubformat_UInt16:
         return pcm_map_from_raw<PcmCode_UInt16, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt16_Be:
+    case PcmSubformat_UInt16_Be:
         return pcm_map_from_raw<PcmCode_UInt16, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt16_Le:
+    case PcmSubformat_UInt16_Le:
         return pcm_map_from_raw<PcmCode_UInt16, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt18:
+    case PcmSubformat_SInt18:
         return pcm_map_from_raw<PcmCode_SInt18, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt18_Be:
+    case PcmSubformat_SInt18_Be:
         return pcm_map_from_raw<PcmCode_SInt18, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt18_Le:
+    case PcmSubformat_SInt18_Le:
         return pcm_map_from_raw<PcmCode_SInt18, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt18:
+    case PcmSubformat_UInt18:
         return pcm_map_from_raw<PcmCode_UInt18, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt18_Be:
+    case PcmSubformat_UInt18_Be:
         return pcm_map_from_raw<PcmCode_UInt18, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt18_Le:
+    case PcmSubformat_UInt18_Le:
         return pcm_map_from_raw<PcmCode_UInt18, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt18_3:
+    case PcmSubformat_SInt18_3:
         return pcm_map_from_raw<PcmCode_SInt18_3, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt18_3_Be:
+    case PcmSubformat_SInt18_3_Be:
         return pcm_map_from_raw<PcmCode_SInt18_3, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt18_3_Le:
+    case PcmSubformat_SInt18_3_Le:
         return pcm_map_from_raw<PcmCode_SInt18_3, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt18_3:
+    case PcmSubformat_UInt18_3:
         return pcm_map_from_raw<PcmCode_UInt18_3, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt18_3_Be:
+    case PcmSubformat_UInt18_3_Be:
         return pcm_map_from_raw<PcmCode_UInt18_3, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt18_3_Le:
+    case PcmSubformat_UInt18_3_Le:
         return pcm_map_from_raw<PcmCode_UInt18_3, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt18_4:
+    case PcmSubformat_SInt18_4:
         return pcm_map_from_raw<PcmCode_SInt18_4, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt18_4_Be:
+    case PcmSubformat_SInt18_4_Be:
         return pcm_map_from_raw<PcmCode_SInt18_4, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt18_4_Le:
+    case PcmSubformat_SInt18_4_Le:
         return pcm_map_from_raw<PcmCode_SInt18_4, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt18_4:
+    case PcmSubformat_UInt18_4:
         return pcm_map_from_raw<PcmCode_UInt18_4, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt18_4_Be:
+    case PcmSubformat_UInt18_4_Be:
         return pcm_map_from_raw<PcmCode_UInt18_4, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt18_4_Le:
+    case PcmSubformat_UInt18_4_Le:
         return pcm_map_from_raw<PcmCode_UInt18_4, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt20:
+    case PcmSubformat_SInt20:
         return pcm_map_from_raw<PcmCode_SInt20, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt20_Be:
+    case PcmSubformat_SInt20_Be:
         return pcm_map_from_raw<PcmCode_SInt20, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt20_Le:
+    case PcmSubformat_SInt20_Le:
         return pcm_map_from_raw<PcmCode_SInt20, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt20:
+    case PcmSubformat_UInt20:
         return pcm_map_from_raw<PcmCode_UInt20, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt20_Be:
+    case PcmSubformat_UInt20_Be:
         return pcm_map_from_raw<PcmCode_UInt20, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt20_Le:
+    case PcmSubformat_UInt20_Le:
         return pcm_map_from_raw<PcmCode_UInt20, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt20_3:
+    case PcmSubformat_SInt20_3:
         return pcm_map_from_raw<PcmCode_SInt20_3, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt20_3_Be:
+    case PcmSubformat_SInt20_3_Be:
         return pcm_map_from_raw<PcmCode_SInt20_3, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt20_3_Le:
+    case PcmSubformat_SInt20_3_Le:
         return pcm_map_from_raw<PcmCode_SInt20_3, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt20_3:
+    case PcmSubformat_UInt20_3:
         return pcm_map_from_raw<PcmCode_UInt20_3, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt20_3_Be:
+    case PcmSubformat_UInt20_3_Be:
         return pcm_map_from_raw<PcmCode_UInt20_3, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt20_3_Le:
+    case PcmSubformat_UInt20_3_Le:
         return pcm_map_from_raw<PcmCode_UInt20_3, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt20_4:
+    case PcmSubformat_SInt20_4:
         return pcm_map_from_raw<PcmCode_SInt20_4, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt20_4_Be:
+    case PcmSubformat_SInt20_4_Be:
         return pcm_map_from_raw<PcmCode_SInt20_4, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt20_4_Le:
+    case PcmSubformat_SInt20_4_Le:
         return pcm_map_from_raw<PcmCode_SInt20_4, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt20_4:
+    case PcmSubformat_UInt20_4:
         return pcm_map_from_raw<PcmCode_UInt20_4, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt20_4_Be:
+    case PcmSubformat_UInt20_4_Be:
         return pcm_map_from_raw<PcmCode_UInt20_4, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt20_4_Le:
+    case PcmSubformat_UInt20_4_Le:
         return pcm_map_from_raw<PcmCode_UInt20_4, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt24:
+    case PcmSubformat_SInt24:
         return pcm_map_from_raw<PcmCode_SInt24, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt24_Be:
+    case PcmSubformat_SInt24_Be:
         return pcm_map_from_raw<PcmCode_SInt24, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt24_Le:
+    case PcmSubformat_SInt24_Le:
         return pcm_map_from_raw<PcmCode_SInt24, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt24:
+    case PcmSubformat_UInt24:
         return pcm_map_from_raw<PcmCode_UInt24, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt24_Be:
+    case PcmSubformat_UInt24_Be:
         return pcm_map_from_raw<PcmCode_UInt24, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt24_Le:
+    case PcmSubformat_UInt24_Le:
         return pcm_map_from_raw<PcmCode_UInt24, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt24_4:
+    case PcmSubformat_SInt24_4:
         return pcm_map_from_raw<PcmCode_SInt24_4, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt24_4_Be:
+    case PcmSubformat_SInt24_4_Be:
         return pcm_map_from_raw<PcmCode_SInt24_4, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt24_4_Le:
+    case PcmSubformat_SInt24_4_Le:
         return pcm_map_from_raw<PcmCode_SInt24_4, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt24_4:
+    case PcmSubformat_UInt24_4:
         return pcm_map_from_raw<PcmCode_UInt24_4, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt24_4_Be:
+    case PcmSubformat_UInt24_4_Be:
         return pcm_map_from_raw<PcmCode_UInt24_4, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt24_4_Le:
+    case PcmSubformat_UInt24_4_Le:
         return pcm_map_from_raw<PcmCode_UInt24_4, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt32:
+    case PcmSubformat_SInt32:
         return pcm_map_from_raw<PcmCode_SInt32, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt32_Be:
+    case PcmSubformat_SInt32_Be:
         return pcm_map_from_raw<PcmCode_SInt32, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt32_Le:
+    case PcmSubformat_SInt32_Le:
         return pcm_map_from_raw<PcmCode_SInt32, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt32:
+    case PcmSubformat_UInt32:
         return pcm_map_from_raw<PcmCode_UInt32, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt32_Be:
+    case PcmSubformat_UInt32_Be:
         return pcm_map_from_raw<PcmCode_UInt32, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt32_Le:
+    case PcmSubformat_UInt32_Le:
         return pcm_map_from_raw<PcmCode_UInt32, PcmEndian_Little>(in_format);
-    case PcmFormat_SInt64:
+    case PcmSubformat_SInt64:
         return pcm_map_from_raw<PcmCode_SInt64, PcmEndian_Default>(in_format);
-    case PcmFormat_SInt64_Be:
+    case PcmSubformat_SInt64_Be:
         return pcm_map_from_raw<PcmCode_SInt64, PcmEndian_Big>(in_format);
-    case PcmFormat_SInt64_Le:
+    case PcmSubformat_SInt64_Le:
         return pcm_map_from_raw<PcmCode_SInt64, PcmEndian_Little>(in_format);
-    case PcmFormat_UInt64:
+    case PcmSubformat_UInt64:
         return pcm_map_from_raw<PcmCode_UInt64, PcmEndian_Default>(in_format);
-    case PcmFormat_UInt64_Be:
+    case PcmSubformat_UInt64_Be:
         return pcm_map_from_raw<PcmCode_UInt64, PcmEndian_Big>(in_format);
-    case PcmFormat_UInt64_Le:
+    case PcmSubformat_UInt64_Le:
         return pcm_map_from_raw<PcmCode_UInt64, PcmEndian_Little>(in_format);
 #if ROC_CPU_ENDIAN != ROC_CPU_BE
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32_Be:
         return pcm_map_from_raw<PcmCode_Float32, PcmEndian_Big>(in_format);
 #else
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32_Le:
         return pcm_map_from_raw<PcmCode_Float32, PcmEndian_Little>(in_format);
 #endif
-    case PcmFormat_Float64:
+    case PcmSubformat_Float64:
         return pcm_map_from_raw<PcmCode_Float64, PcmEndian_Default>(in_format);
-    case PcmFormat_Float64_Be:
+    case PcmSubformat_Float64_Be:
         return pcm_map_from_raw<PcmCode_Float64, PcmEndian_Big>(in_format);
-    case PcmFormat_Float64_Le:
+    case PcmSubformat_Float64_Le:
         return pcm_map_from_raw<PcmCode_Float64, PcmEndian_Little>(in_format);
     default:
         break;
@@ -4543,13 +4495,13 @@ PcmMapFn pcm_format_mapfn(PcmFormat in_format, PcmFormat out_format) {
 
     // raw to raw
     switch (out_format) {
-    case PcmFormat_Float32:
+    case PcmSubformat_Float32:
         return pcm_map_from_raw<PcmCode_Float32, PcmEndian_Default>(in_format);
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32_Be:
         return pcm_map_from_raw<PcmCode_Float32, PcmEndian_Default>(in_format);
 #else
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32_Le:
         return pcm_map_from_raw<PcmCode_Float32, PcmEndian_Default>(in_format);
 #endif
     default:
@@ -4560,27 +4512,31 @@ PcmMapFn pcm_format_mapfn(PcmFormat in_format, PcmFormat out_format) {
 }
 
 // Get format traits
-PcmTraits pcm_format_traits(PcmFormat format) {
+PcmTraits pcm_subformat_traits(PcmSubformat format) {
     PcmTraits traits;
 
     switch (format) {
-    case PcmFormat_SInt8:
+    case PcmSubformat_SInt8:
+        traits.id = PcmSubformat_SInt8;
+        traits.name = "s8";
         traits.bit_width = 8;
         traits.bit_depth = 8;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt8_Be;
+        traits.portable_alias = PcmSubformat_SInt8_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt8_Le;
+        traits.portable_alias = PcmSubformat_SInt8_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt8;
-        traits.be_variant = PcmFormat_SInt8_Be;
-        traits.le_variant = PcmFormat_SInt8_Le;
+        traits.default_variant = PcmSubformat_SInt8;
+        traits.be_variant = PcmSubformat_SInt8_Be;
+        traits.le_variant = PcmSubformat_SInt8_Le;
         break;
 
-    case PcmFormat_SInt8_Be:
+    case PcmSubformat_SInt8_Be:
+        traits.id = PcmSubformat_SInt8_Be;
+        traits.name = "s8_be";
         traits.bit_width = 8;
         traits.bit_depth = 8;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -4589,13 +4545,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt8_Be;
-        traits.default_variant = PcmFormat_SInt8;
-        traits.be_variant = PcmFormat_SInt8_Be;
-        traits.le_variant = PcmFormat_SInt8_Le;
+        traits.portable_alias = PcmSubformat_SInt8_Be;
+        traits.default_variant = PcmSubformat_SInt8;
+        traits.be_variant = PcmSubformat_SInt8_Be;
+        traits.le_variant = PcmSubformat_SInt8_Le;
         break;
 
-    case PcmFormat_SInt8_Le:
+    case PcmSubformat_SInt8_Le:
+        traits.id = PcmSubformat_SInt8_Le;
+        traits.name = "s8_le";
         traits.bit_width = 8;
         traits.bit_depth = 8;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -4604,29 +4562,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt8_Le;
-        traits.default_variant = PcmFormat_SInt8;
-        traits.be_variant = PcmFormat_SInt8_Be;
-        traits.le_variant = PcmFormat_SInt8_Le;
+        traits.portable_alias = PcmSubformat_SInt8_Le;
+        traits.default_variant = PcmSubformat_SInt8;
+        traits.be_variant = PcmSubformat_SInt8_Be;
+        traits.le_variant = PcmSubformat_SInt8_Le;
         break;
 
-    case PcmFormat_UInt8:
+    case PcmSubformat_UInt8:
+        traits.id = PcmSubformat_UInt8;
+        traits.name = "u8";
         traits.bit_width = 8;
         traits.bit_depth = 8;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt8_Be;
+        traits.portable_alias = PcmSubformat_UInt8_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt8_Le;
+        traits.portable_alias = PcmSubformat_UInt8_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt8;
-        traits.be_variant = PcmFormat_UInt8_Be;
-        traits.le_variant = PcmFormat_UInt8_Le;
+        traits.default_variant = PcmSubformat_UInt8;
+        traits.be_variant = PcmSubformat_UInt8_Be;
+        traits.le_variant = PcmSubformat_UInt8_Le;
         break;
 
-    case PcmFormat_UInt8_Be:
+    case PcmSubformat_UInt8_Be:
+        traits.id = PcmSubformat_UInt8_Be;
+        traits.name = "u8_be";
         traits.bit_width = 8;
         traits.bit_depth = 8;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -4635,13 +4597,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt8_Be;
-        traits.default_variant = PcmFormat_UInt8;
-        traits.be_variant = PcmFormat_UInt8_Be;
-        traits.le_variant = PcmFormat_UInt8_Le;
+        traits.portable_alias = PcmSubformat_UInt8_Be;
+        traits.default_variant = PcmSubformat_UInt8;
+        traits.be_variant = PcmSubformat_UInt8_Be;
+        traits.le_variant = PcmSubformat_UInt8_Le;
         break;
 
-    case PcmFormat_UInt8_Le:
+    case PcmSubformat_UInt8_Le:
+        traits.id = PcmSubformat_UInt8_Le;
+        traits.name = "u8_le";
         traits.bit_width = 8;
         traits.bit_depth = 8;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -4650,29 +4614,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt8_Le;
-        traits.default_variant = PcmFormat_UInt8;
-        traits.be_variant = PcmFormat_UInt8_Be;
-        traits.le_variant = PcmFormat_UInt8_Le;
+        traits.portable_alias = PcmSubformat_UInt8_Le;
+        traits.default_variant = PcmSubformat_UInt8;
+        traits.be_variant = PcmSubformat_UInt8_Be;
+        traits.le_variant = PcmSubformat_UInt8_Le;
         break;
 
-    case PcmFormat_SInt16:
+    case PcmSubformat_SInt16:
+        traits.id = PcmSubformat_SInt16;
+        traits.name = "s16";
         traits.bit_width = 16;
         traits.bit_depth = 16;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt16_Be;
+        traits.portable_alias = PcmSubformat_SInt16_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt16_Le;
+        traits.portable_alias = PcmSubformat_SInt16_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt16;
-        traits.be_variant = PcmFormat_SInt16_Be;
-        traits.le_variant = PcmFormat_SInt16_Le;
+        traits.default_variant = PcmSubformat_SInt16;
+        traits.be_variant = PcmSubformat_SInt16_Be;
+        traits.le_variant = PcmSubformat_SInt16_Le;
         break;
 
-    case PcmFormat_SInt16_Be:
+    case PcmSubformat_SInt16_Be:
+        traits.id = PcmSubformat_SInt16_Be;
+        traits.name = "s16_be";
         traits.bit_width = 16;
         traits.bit_depth = 16;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -4681,13 +4649,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt16_Be;
-        traits.default_variant = PcmFormat_SInt16;
-        traits.be_variant = PcmFormat_SInt16_Be;
-        traits.le_variant = PcmFormat_SInt16_Le;
+        traits.portable_alias = PcmSubformat_SInt16_Be;
+        traits.default_variant = PcmSubformat_SInt16;
+        traits.be_variant = PcmSubformat_SInt16_Be;
+        traits.le_variant = PcmSubformat_SInt16_Le;
         break;
 
-    case PcmFormat_SInt16_Le:
+    case PcmSubformat_SInt16_Le:
+        traits.id = PcmSubformat_SInt16_Le;
+        traits.name = "s16_le";
         traits.bit_width = 16;
         traits.bit_depth = 16;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -4696,29 +4666,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt16_Le;
-        traits.default_variant = PcmFormat_SInt16;
-        traits.be_variant = PcmFormat_SInt16_Be;
-        traits.le_variant = PcmFormat_SInt16_Le;
+        traits.portable_alias = PcmSubformat_SInt16_Le;
+        traits.default_variant = PcmSubformat_SInt16;
+        traits.be_variant = PcmSubformat_SInt16_Be;
+        traits.le_variant = PcmSubformat_SInt16_Le;
         break;
 
-    case PcmFormat_UInt16:
+    case PcmSubformat_UInt16:
+        traits.id = PcmSubformat_UInt16;
+        traits.name = "u16";
         traits.bit_width = 16;
         traits.bit_depth = 16;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt16_Be;
+        traits.portable_alias = PcmSubformat_UInt16_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt16_Le;
+        traits.portable_alias = PcmSubformat_UInt16_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt16;
-        traits.be_variant = PcmFormat_UInt16_Be;
-        traits.le_variant = PcmFormat_UInt16_Le;
+        traits.default_variant = PcmSubformat_UInt16;
+        traits.be_variant = PcmSubformat_UInt16_Be;
+        traits.le_variant = PcmSubformat_UInt16_Le;
         break;
 
-    case PcmFormat_UInt16_Be:
+    case PcmSubformat_UInt16_Be:
+        traits.id = PcmSubformat_UInt16_Be;
+        traits.name = "u16_be";
         traits.bit_width = 16;
         traits.bit_depth = 16;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -4727,13 +4701,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt16_Be;
-        traits.default_variant = PcmFormat_UInt16;
-        traits.be_variant = PcmFormat_UInt16_Be;
-        traits.le_variant = PcmFormat_UInt16_Le;
+        traits.portable_alias = PcmSubformat_UInt16_Be;
+        traits.default_variant = PcmSubformat_UInt16;
+        traits.be_variant = PcmSubformat_UInt16_Be;
+        traits.le_variant = PcmSubformat_UInt16_Le;
         break;
 
-    case PcmFormat_UInt16_Le:
+    case PcmSubformat_UInt16_Le:
+        traits.id = PcmSubformat_UInt16_Le;
+        traits.name = "u16_le";
         traits.bit_width = 16;
         traits.bit_depth = 16;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -4742,29 +4718,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt16_Le;
-        traits.default_variant = PcmFormat_UInt16;
-        traits.be_variant = PcmFormat_UInt16_Be;
-        traits.le_variant = PcmFormat_UInt16_Le;
+        traits.portable_alias = PcmSubformat_UInt16_Le;
+        traits.default_variant = PcmSubformat_UInt16;
+        traits.be_variant = PcmSubformat_UInt16_Be;
+        traits.le_variant = PcmSubformat_UInt16_Le;
         break;
 
-    case PcmFormat_SInt18:
+    case PcmSubformat_SInt18:
+        traits.id = PcmSubformat_SInt18;
+        traits.name = "s18";
         traits.bit_width = 18;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt18_Be;
+        traits.portable_alias = PcmSubformat_SInt18_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt18_Le;
+        traits.portable_alias = PcmSubformat_SInt18_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt18;
-        traits.be_variant = PcmFormat_SInt18_Be;
-        traits.le_variant = PcmFormat_SInt18_Le;
+        traits.default_variant = PcmSubformat_SInt18;
+        traits.be_variant = PcmSubformat_SInt18_Be;
+        traits.le_variant = PcmSubformat_SInt18_Le;
         break;
 
-    case PcmFormat_SInt18_Be:
+    case PcmSubformat_SInt18_Be:
+        traits.id = PcmSubformat_SInt18_Be;
+        traits.name = "s18_be";
         traits.bit_width = 18;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked;
@@ -4773,13 +4753,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt18_Be;
-        traits.default_variant = PcmFormat_SInt18;
-        traits.be_variant = PcmFormat_SInt18_Be;
-        traits.le_variant = PcmFormat_SInt18_Le;
+        traits.portable_alias = PcmSubformat_SInt18_Be;
+        traits.default_variant = PcmSubformat_SInt18;
+        traits.be_variant = PcmSubformat_SInt18_Be;
+        traits.le_variant = PcmSubformat_SInt18_Le;
         break;
 
-    case PcmFormat_SInt18_Le:
+    case PcmSubformat_SInt18_Le:
+        traits.id = PcmSubformat_SInt18_Le;
+        traits.name = "s18_le";
         traits.bit_width = 18;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked;
@@ -4788,29 +4770,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt18_Le;
-        traits.default_variant = PcmFormat_SInt18;
-        traits.be_variant = PcmFormat_SInt18_Be;
-        traits.le_variant = PcmFormat_SInt18_Le;
+        traits.portable_alias = PcmSubformat_SInt18_Le;
+        traits.default_variant = PcmSubformat_SInt18;
+        traits.be_variant = PcmSubformat_SInt18_Be;
+        traits.le_variant = PcmSubformat_SInt18_Le;
         break;
 
-    case PcmFormat_UInt18:
+    case PcmSubformat_UInt18:
+        traits.id = PcmSubformat_UInt18;
+        traits.name = "u18";
         traits.bit_width = 18;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt18_Be;
+        traits.portable_alias = PcmSubformat_UInt18_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt18_Le;
+        traits.portable_alias = PcmSubformat_UInt18_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt18;
-        traits.be_variant = PcmFormat_UInt18_Be;
-        traits.le_variant = PcmFormat_UInt18_Le;
+        traits.default_variant = PcmSubformat_UInt18;
+        traits.be_variant = PcmSubformat_UInt18_Be;
+        traits.le_variant = PcmSubformat_UInt18_Le;
         break;
 
-    case PcmFormat_UInt18_Be:
+    case PcmSubformat_UInt18_Be:
+        traits.id = PcmSubformat_UInt18_Be;
+        traits.name = "u18_be";
         traits.bit_width = 18;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked;
@@ -4819,13 +4805,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt18_Be;
-        traits.default_variant = PcmFormat_UInt18;
-        traits.be_variant = PcmFormat_UInt18_Be;
-        traits.le_variant = PcmFormat_UInt18_Le;
+        traits.portable_alias = PcmSubformat_UInt18_Be;
+        traits.default_variant = PcmSubformat_UInt18;
+        traits.be_variant = PcmSubformat_UInt18_Be;
+        traits.le_variant = PcmSubformat_UInt18_Le;
         break;
 
-    case PcmFormat_UInt18_Le:
+    case PcmSubformat_UInt18_Le:
+        traits.id = PcmSubformat_UInt18_Le;
+        traits.name = "u18_le";
         traits.bit_width = 18;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked;
@@ -4834,29 +4822,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt18_Le;
-        traits.default_variant = PcmFormat_UInt18;
-        traits.be_variant = PcmFormat_UInt18_Be;
-        traits.le_variant = PcmFormat_UInt18_Le;
+        traits.portable_alias = PcmSubformat_UInt18_Le;
+        traits.default_variant = PcmSubformat_UInt18;
+        traits.be_variant = PcmSubformat_UInt18_Be;
+        traits.le_variant = PcmSubformat_UInt18_Le;
         break;
 
-    case PcmFormat_SInt18_3:
+    case PcmSubformat_SInt18_3:
+        traits.id = PcmSubformat_SInt18_3;
+        traits.name = "s18_3";
         traits.bit_width = 24;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt18_3_Be;
+        traits.portable_alias = PcmSubformat_SInt18_3_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt18_3_Le;
+        traits.portable_alias = PcmSubformat_SInt18_3_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt18_3;
-        traits.be_variant = PcmFormat_SInt18_3_Be;
-        traits.le_variant = PcmFormat_SInt18_3_Le;
+        traits.default_variant = PcmSubformat_SInt18_3;
+        traits.be_variant = PcmSubformat_SInt18_3_Be;
+        traits.le_variant = PcmSubformat_SInt18_3_Le;
         break;
 
-    case PcmFormat_SInt18_3_Be:
+    case PcmSubformat_SInt18_3_Be:
+        traits.id = PcmSubformat_SInt18_3_Be;
+        traits.name = "s18_3be";
         traits.bit_width = 24;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -4865,13 +4857,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt18_3_Be;
-        traits.default_variant = PcmFormat_SInt18_3;
-        traits.be_variant = PcmFormat_SInt18_3_Be;
-        traits.le_variant = PcmFormat_SInt18_3_Le;
+        traits.portable_alias = PcmSubformat_SInt18_3_Be;
+        traits.default_variant = PcmSubformat_SInt18_3;
+        traits.be_variant = PcmSubformat_SInt18_3_Be;
+        traits.le_variant = PcmSubformat_SInt18_3_Le;
         break;
 
-    case PcmFormat_SInt18_3_Le:
+    case PcmSubformat_SInt18_3_Le:
+        traits.id = PcmSubformat_SInt18_3_Le;
+        traits.name = "s18_3le";
         traits.bit_width = 24;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -4880,29 +4874,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt18_3_Le;
-        traits.default_variant = PcmFormat_SInt18_3;
-        traits.be_variant = PcmFormat_SInt18_3_Be;
-        traits.le_variant = PcmFormat_SInt18_3_Le;
+        traits.portable_alias = PcmSubformat_SInt18_3_Le;
+        traits.default_variant = PcmSubformat_SInt18_3;
+        traits.be_variant = PcmSubformat_SInt18_3_Be;
+        traits.le_variant = PcmSubformat_SInt18_3_Le;
         break;
 
-    case PcmFormat_UInt18_3:
+    case PcmSubformat_UInt18_3:
+        traits.id = PcmSubformat_UInt18_3;
+        traits.name = "u18_3";
         traits.bit_width = 24;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt18_3_Be;
+        traits.portable_alias = PcmSubformat_UInt18_3_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt18_3_Le;
+        traits.portable_alias = PcmSubformat_UInt18_3_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt18_3;
-        traits.be_variant = PcmFormat_UInt18_3_Be;
-        traits.le_variant = PcmFormat_UInt18_3_Le;
+        traits.default_variant = PcmSubformat_UInt18_3;
+        traits.be_variant = PcmSubformat_UInt18_3_Be;
+        traits.le_variant = PcmSubformat_UInt18_3_Le;
         break;
 
-    case PcmFormat_UInt18_3_Be:
+    case PcmSubformat_UInt18_3_Be:
+        traits.id = PcmSubformat_UInt18_3_Be;
+        traits.name = "u18_3be";
         traits.bit_width = 24;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger;
@@ -4911,13 +4909,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt18_3_Be;
-        traits.default_variant = PcmFormat_UInt18_3;
-        traits.be_variant = PcmFormat_UInt18_3_Be;
-        traits.le_variant = PcmFormat_UInt18_3_Le;
+        traits.portable_alias = PcmSubformat_UInt18_3_Be;
+        traits.default_variant = PcmSubformat_UInt18_3;
+        traits.be_variant = PcmSubformat_UInt18_3_Be;
+        traits.le_variant = PcmSubformat_UInt18_3_Le;
         break;
 
-    case PcmFormat_UInt18_3_Le:
+    case PcmSubformat_UInt18_3_Le:
+        traits.id = PcmSubformat_UInt18_3_Le;
+        traits.name = "u18_3le";
         traits.bit_width = 24;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger;
@@ -4926,29 +4926,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt18_3_Le;
-        traits.default_variant = PcmFormat_UInt18_3;
-        traits.be_variant = PcmFormat_UInt18_3_Be;
-        traits.le_variant = PcmFormat_UInt18_3_Le;
+        traits.portable_alias = PcmSubformat_UInt18_3_Le;
+        traits.default_variant = PcmSubformat_UInt18_3;
+        traits.be_variant = PcmSubformat_UInt18_3_Be;
+        traits.le_variant = PcmSubformat_UInt18_3_Le;
         break;
 
-    case PcmFormat_SInt18_4:
+    case PcmSubformat_SInt18_4:
+        traits.id = PcmSubformat_SInt18_4;
+        traits.name = "s18_4";
         traits.bit_width = 32;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt18_4_Be;
+        traits.portable_alias = PcmSubformat_SInt18_4_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt18_4_Le;
+        traits.portable_alias = PcmSubformat_SInt18_4_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt18_4;
-        traits.be_variant = PcmFormat_SInt18_4_Be;
-        traits.le_variant = PcmFormat_SInt18_4_Le;
+        traits.default_variant = PcmSubformat_SInt18_4;
+        traits.be_variant = PcmSubformat_SInt18_4_Be;
+        traits.le_variant = PcmSubformat_SInt18_4_Le;
         break;
 
-    case PcmFormat_SInt18_4_Be:
+    case PcmSubformat_SInt18_4_Be:
+        traits.id = PcmSubformat_SInt18_4_Be;
+        traits.name = "s18_4be";
         traits.bit_width = 32;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -4957,13 +4961,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt18_4_Be;
-        traits.default_variant = PcmFormat_SInt18_4;
-        traits.be_variant = PcmFormat_SInt18_4_Be;
-        traits.le_variant = PcmFormat_SInt18_4_Le;
+        traits.portable_alias = PcmSubformat_SInt18_4_Be;
+        traits.default_variant = PcmSubformat_SInt18_4;
+        traits.be_variant = PcmSubformat_SInt18_4_Be;
+        traits.le_variant = PcmSubformat_SInt18_4_Le;
         break;
 
-    case PcmFormat_SInt18_4_Le:
+    case PcmSubformat_SInt18_4_Le:
+        traits.id = PcmSubformat_SInt18_4_Le;
+        traits.name = "s18_4le";
         traits.bit_width = 32;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -4972,29 +4978,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt18_4_Le;
-        traits.default_variant = PcmFormat_SInt18_4;
-        traits.be_variant = PcmFormat_SInt18_4_Be;
-        traits.le_variant = PcmFormat_SInt18_4_Le;
+        traits.portable_alias = PcmSubformat_SInt18_4_Le;
+        traits.default_variant = PcmSubformat_SInt18_4;
+        traits.be_variant = PcmSubformat_SInt18_4_Be;
+        traits.le_variant = PcmSubformat_SInt18_4_Le;
         break;
 
-    case PcmFormat_UInt18_4:
+    case PcmSubformat_UInt18_4:
+        traits.id = PcmSubformat_UInt18_4;
+        traits.name = "u18_4";
         traits.bit_width = 32;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt18_4_Be;
+        traits.portable_alias = PcmSubformat_UInt18_4_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt18_4_Le;
+        traits.portable_alias = PcmSubformat_UInt18_4_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt18_4;
-        traits.be_variant = PcmFormat_UInt18_4_Be;
-        traits.le_variant = PcmFormat_UInt18_4_Le;
+        traits.default_variant = PcmSubformat_UInt18_4;
+        traits.be_variant = PcmSubformat_UInt18_4_Be;
+        traits.le_variant = PcmSubformat_UInt18_4_Le;
         break;
 
-    case PcmFormat_UInt18_4_Be:
+    case PcmSubformat_UInt18_4_Be:
+        traits.id = PcmSubformat_UInt18_4_Be;
+        traits.name = "u18_4be";
         traits.bit_width = 32;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger;
@@ -5003,13 +5013,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt18_4_Be;
-        traits.default_variant = PcmFormat_UInt18_4;
-        traits.be_variant = PcmFormat_UInt18_4_Be;
-        traits.le_variant = PcmFormat_UInt18_4_Le;
+        traits.portable_alias = PcmSubformat_UInt18_4_Be;
+        traits.default_variant = PcmSubformat_UInt18_4;
+        traits.be_variant = PcmSubformat_UInt18_4_Be;
+        traits.le_variant = PcmSubformat_UInt18_4_Le;
         break;
 
-    case PcmFormat_UInt18_4_Le:
+    case PcmSubformat_UInt18_4_Le:
+        traits.id = PcmSubformat_UInt18_4_Le;
+        traits.name = "u18_4le";
         traits.bit_width = 32;
         traits.bit_depth = 18;
         traits.flags = Pcm_IsInteger;
@@ -5018,29 +5030,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt18_4_Le;
-        traits.default_variant = PcmFormat_UInt18_4;
-        traits.be_variant = PcmFormat_UInt18_4_Be;
-        traits.le_variant = PcmFormat_UInt18_4_Le;
+        traits.portable_alias = PcmSubformat_UInt18_4_Le;
+        traits.default_variant = PcmSubformat_UInt18_4;
+        traits.be_variant = PcmSubformat_UInt18_4_Be;
+        traits.le_variant = PcmSubformat_UInt18_4_Le;
         break;
 
-    case PcmFormat_SInt20:
+    case PcmSubformat_SInt20:
+        traits.id = PcmSubformat_SInt20;
+        traits.name = "s20";
         traits.bit_width = 20;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt20_Be;
+        traits.portable_alias = PcmSubformat_SInt20_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt20_Le;
+        traits.portable_alias = PcmSubformat_SInt20_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt20;
-        traits.be_variant = PcmFormat_SInt20_Be;
-        traits.le_variant = PcmFormat_SInt20_Le;
+        traits.default_variant = PcmSubformat_SInt20;
+        traits.be_variant = PcmSubformat_SInt20_Be;
+        traits.le_variant = PcmSubformat_SInt20_Le;
         break;
 
-    case PcmFormat_SInt20_Be:
+    case PcmSubformat_SInt20_Be:
+        traits.id = PcmSubformat_SInt20_Be;
+        traits.name = "s20_be";
         traits.bit_width = 20;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked;
@@ -5049,13 +5065,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt20_Be;
-        traits.default_variant = PcmFormat_SInt20;
-        traits.be_variant = PcmFormat_SInt20_Be;
-        traits.le_variant = PcmFormat_SInt20_Le;
+        traits.portable_alias = PcmSubformat_SInt20_Be;
+        traits.default_variant = PcmSubformat_SInt20;
+        traits.be_variant = PcmSubformat_SInt20_Be;
+        traits.le_variant = PcmSubformat_SInt20_Le;
         break;
 
-    case PcmFormat_SInt20_Le:
+    case PcmSubformat_SInt20_Le:
+        traits.id = PcmSubformat_SInt20_Le;
+        traits.name = "s20_le";
         traits.bit_width = 20;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked;
@@ -5064,29 +5082,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt20_Le;
-        traits.default_variant = PcmFormat_SInt20;
-        traits.be_variant = PcmFormat_SInt20_Be;
-        traits.le_variant = PcmFormat_SInt20_Le;
+        traits.portable_alias = PcmSubformat_SInt20_Le;
+        traits.default_variant = PcmSubformat_SInt20;
+        traits.be_variant = PcmSubformat_SInt20_Be;
+        traits.le_variant = PcmSubformat_SInt20_Le;
         break;
 
-    case PcmFormat_UInt20:
+    case PcmSubformat_UInt20:
+        traits.id = PcmSubformat_UInt20;
+        traits.name = "u20";
         traits.bit_width = 20;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt20_Be;
+        traits.portable_alias = PcmSubformat_UInt20_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt20_Le;
+        traits.portable_alias = PcmSubformat_UInt20_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt20;
-        traits.be_variant = PcmFormat_UInt20_Be;
-        traits.le_variant = PcmFormat_UInt20_Le;
+        traits.default_variant = PcmSubformat_UInt20;
+        traits.be_variant = PcmSubformat_UInt20_Be;
+        traits.le_variant = PcmSubformat_UInt20_Le;
         break;
 
-    case PcmFormat_UInt20_Be:
+    case PcmSubformat_UInt20_Be:
+        traits.id = PcmSubformat_UInt20_Be;
+        traits.name = "u20_be";
         traits.bit_width = 20;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked;
@@ -5095,13 +5117,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt20_Be;
-        traits.default_variant = PcmFormat_UInt20;
-        traits.be_variant = PcmFormat_UInt20_Be;
-        traits.le_variant = PcmFormat_UInt20_Le;
+        traits.portable_alias = PcmSubformat_UInt20_Be;
+        traits.default_variant = PcmSubformat_UInt20;
+        traits.be_variant = PcmSubformat_UInt20_Be;
+        traits.le_variant = PcmSubformat_UInt20_Le;
         break;
 
-    case PcmFormat_UInt20_Le:
+    case PcmSubformat_UInt20_Le:
+        traits.id = PcmSubformat_UInt20_Le;
+        traits.name = "u20_le";
         traits.bit_width = 20;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked;
@@ -5110,29 +5134,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt20_Le;
-        traits.default_variant = PcmFormat_UInt20;
-        traits.be_variant = PcmFormat_UInt20_Be;
-        traits.le_variant = PcmFormat_UInt20_Le;
+        traits.portable_alias = PcmSubformat_UInt20_Le;
+        traits.default_variant = PcmSubformat_UInt20;
+        traits.be_variant = PcmSubformat_UInt20_Be;
+        traits.le_variant = PcmSubformat_UInt20_Le;
         break;
 
-    case PcmFormat_SInt20_3:
+    case PcmSubformat_SInt20_3:
+        traits.id = PcmSubformat_SInt20_3;
+        traits.name = "s20_3";
         traits.bit_width = 24;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt20_3_Be;
+        traits.portable_alias = PcmSubformat_SInt20_3_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt20_3_Le;
+        traits.portable_alias = PcmSubformat_SInt20_3_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt20_3;
-        traits.be_variant = PcmFormat_SInt20_3_Be;
-        traits.le_variant = PcmFormat_SInt20_3_Le;
+        traits.default_variant = PcmSubformat_SInt20_3;
+        traits.be_variant = PcmSubformat_SInt20_3_Be;
+        traits.le_variant = PcmSubformat_SInt20_3_Le;
         break;
 
-    case PcmFormat_SInt20_3_Be:
+    case PcmSubformat_SInt20_3_Be:
+        traits.id = PcmSubformat_SInt20_3_Be;
+        traits.name = "s20_3be";
         traits.bit_width = 24;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -5141,13 +5169,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt20_3_Be;
-        traits.default_variant = PcmFormat_SInt20_3;
-        traits.be_variant = PcmFormat_SInt20_3_Be;
-        traits.le_variant = PcmFormat_SInt20_3_Le;
+        traits.portable_alias = PcmSubformat_SInt20_3_Be;
+        traits.default_variant = PcmSubformat_SInt20_3;
+        traits.be_variant = PcmSubformat_SInt20_3_Be;
+        traits.le_variant = PcmSubformat_SInt20_3_Le;
         break;
 
-    case PcmFormat_SInt20_3_Le:
+    case PcmSubformat_SInt20_3_Le:
+        traits.id = PcmSubformat_SInt20_3_Le;
+        traits.name = "s20_3le";
         traits.bit_width = 24;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -5156,29 +5186,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt20_3_Le;
-        traits.default_variant = PcmFormat_SInt20_3;
-        traits.be_variant = PcmFormat_SInt20_3_Be;
-        traits.le_variant = PcmFormat_SInt20_3_Le;
+        traits.portable_alias = PcmSubformat_SInt20_3_Le;
+        traits.default_variant = PcmSubformat_SInt20_3;
+        traits.be_variant = PcmSubformat_SInt20_3_Be;
+        traits.le_variant = PcmSubformat_SInt20_3_Le;
         break;
 
-    case PcmFormat_UInt20_3:
+    case PcmSubformat_UInt20_3:
+        traits.id = PcmSubformat_UInt20_3;
+        traits.name = "u20_3";
         traits.bit_width = 24;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt20_3_Be;
+        traits.portable_alias = PcmSubformat_UInt20_3_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt20_3_Le;
+        traits.portable_alias = PcmSubformat_UInt20_3_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt20_3;
-        traits.be_variant = PcmFormat_UInt20_3_Be;
-        traits.le_variant = PcmFormat_UInt20_3_Le;
+        traits.default_variant = PcmSubformat_UInt20_3;
+        traits.be_variant = PcmSubformat_UInt20_3_Be;
+        traits.le_variant = PcmSubformat_UInt20_3_Le;
         break;
 
-    case PcmFormat_UInt20_3_Be:
+    case PcmSubformat_UInt20_3_Be:
+        traits.id = PcmSubformat_UInt20_3_Be;
+        traits.name = "u20_3be";
         traits.bit_width = 24;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger;
@@ -5187,13 +5221,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt20_3_Be;
-        traits.default_variant = PcmFormat_UInt20_3;
-        traits.be_variant = PcmFormat_UInt20_3_Be;
-        traits.le_variant = PcmFormat_UInt20_3_Le;
+        traits.portable_alias = PcmSubformat_UInt20_3_Be;
+        traits.default_variant = PcmSubformat_UInt20_3;
+        traits.be_variant = PcmSubformat_UInt20_3_Be;
+        traits.le_variant = PcmSubformat_UInt20_3_Le;
         break;
 
-    case PcmFormat_UInt20_3_Le:
+    case PcmSubformat_UInt20_3_Le:
+        traits.id = PcmSubformat_UInt20_3_Le;
+        traits.name = "u20_3le";
         traits.bit_width = 24;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger;
@@ -5202,29 +5238,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt20_3_Le;
-        traits.default_variant = PcmFormat_UInt20_3;
-        traits.be_variant = PcmFormat_UInt20_3_Be;
-        traits.le_variant = PcmFormat_UInt20_3_Le;
+        traits.portable_alias = PcmSubformat_UInt20_3_Le;
+        traits.default_variant = PcmSubformat_UInt20_3;
+        traits.be_variant = PcmSubformat_UInt20_3_Be;
+        traits.le_variant = PcmSubformat_UInt20_3_Le;
         break;
 
-    case PcmFormat_SInt20_4:
+    case PcmSubformat_SInt20_4:
+        traits.id = PcmSubformat_SInt20_4;
+        traits.name = "s20_4";
         traits.bit_width = 32;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt20_4_Be;
+        traits.portable_alias = PcmSubformat_SInt20_4_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt20_4_Le;
+        traits.portable_alias = PcmSubformat_SInt20_4_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt20_4;
-        traits.be_variant = PcmFormat_SInt20_4_Be;
-        traits.le_variant = PcmFormat_SInt20_4_Le;
+        traits.default_variant = PcmSubformat_SInt20_4;
+        traits.be_variant = PcmSubformat_SInt20_4_Be;
+        traits.le_variant = PcmSubformat_SInt20_4_Le;
         break;
 
-    case PcmFormat_SInt20_4_Be:
+    case PcmSubformat_SInt20_4_Be:
+        traits.id = PcmSubformat_SInt20_4_Be;
+        traits.name = "s20_4be";
         traits.bit_width = 32;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -5233,13 +5273,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt20_4_Be;
-        traits.default_variant = PcmFormat_SInt20_4;
-        traits.be_variant = PcmFormat_SInt20_4_Be;
-        traits.le_variant = PcmFormat_SInt20_4_Le;
+        traits.portable_alias = PcmSubformat_SInt20_4_Be;
+        traits.default_variant = PcmSubformat_SInt20_4;
+        traits.be_variant = PcmSubformat_SInt20_4_Be;
+        traits.le_variant = PcmSubformat_SInt20_4_Le;
         break;
 
-    case PcmFormat_SInt20_4_Le:
+    case PcmSubformat_SInt20_4_Le:
+        traits.id = PcmSubformat_SInt20_4_Le;
+        traits.name = "s20_4le";
         traits.bit_width = 32;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned;
@@ -5248,29 +5290,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt20_4_Le;
-        traits.default_variant = PcmFormat_SInt20_4;
-        traits.be_variant = PcmFormat_SInt20_4_Be;
-        traits.le_variant = PcmFormat_SInt20_4_Le;
+        traits.portable_alias = PcmSubformat_SInt20_4_Le;
+        traits.default_variant = PcmSubformat_SInt20_4;
+        traits.be_variant = PcmSubformat_SInt20_4_Be;
+        traits.le_variant = PcmSubformat_SInt20_4_Le;
         break;
 
-    case PcmFormat_UInt20_4:
+    case PcmSubformat_UInt20_4:
+        traits.id = PcmSubformat_UInt20_4;
+        traits.name = "u20_4";
         traits.bit_width = 32;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt20_4_Be;
+        traits.portable_alias = PcmSubformat_UInt20_4_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt20_4_Le;
+        traits.portable_alias = PcmSubformat_UInt20_4_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt20_4;
-        traits.be_variant = PcmFormat_UInt20_4_Be;
-        traits.le_variant = PcmFormat_UInt20_4_Le;
+        traits.default_variant = PcmSubformat_UInt20_4;
+        traits.be_variant = PcmSubformat_UInt20_4_Be;
+        traits.le_variant = PcmSubformat_UInt20_4_Le;
         break;
 
-    case PcmFormat_UInt20_4_Be:
+    case PcmSubformat_UInt20_4_Be:
+        traits.id = PcmSubformat_UInt20_4_Be;
+        traits.name = "u20_4be";
         traits.bit_width = 32;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger;
@@ -5279,13 +5325,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt20_4_Be;
-        traits.default_variant = PcmFormat_UInt20_4;
-        traits.be_variant = PcmFormat_UInt20_4_Be;
-        traits.le_variant = PcmFormat_UInt20_4_Le;
+        traits.portable_alias = PcmSubformat_UInt20_4_Be;
+        traits.default_variant = PcmSubformat_UInt20_4;
+        traits.be_variant = PcmSubformat_UInt20_4_Be;
+        traits.le_variant = PcmSubformat_UInt20_4_Le;
         break;
 
-    case PcmFormat_UInt20_4_Le:
+    case PcmSubformat_UInt20_4_Le:
+        traits.id = PcmSubformat_UInt20_4_Le;
+        traits.name = "u20_4le";
         traits.bit_width = 32;
         traits.bit_depth = 20;
         traits.flags = Pcm_IsInteger;
@@ -5294,29 +5342,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt20_4_Le;
-        traits.default_variant = PcmFormat_UInt20_4;
-        traits.be_variant = PcmFormat_UInt20_4_Be;
-        traits.le_variant = PcmFormat_UInt20_4_Le;
+        traits.portable_alias = PcmSubformat_UInt20_4_Le;
+        traits.default_variant = PcmSubformat_UInt20_4;
+        traits.be_variant = PcmSubformat_UInt20_4_Be;
+        traits.le_variant = PcmSubformat_UInt20_4_Le;
         break;
 
-    case PcmFormat_SInt24:
+    case PcmSubformat_SInt24:
+        traits.id = PcmSubformat_SInt24;
+        traits.name = "s24";
         traits.bit_width = 24;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt24_Be;
+        traits.portable_alias = PcmSubformat_SInt24_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt24_Le;
+        traits.portable_alias = PcmSubformat_SInt24_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt24;
-        traits.be_variant = PcmFormat_SInt24_Be;
-        traits.le_variant = PcmFormat_SInt24_Le;
+        traits.default_variant = PcmSubformat_SInt24;
+        traits.be_variant = PcmSubformat_SInt24_Be;
+        traits.le_variant = PcmSubformat_SInt24_Le;
         break;
 
-    case PcmFormat_SInt24_Be:
+    case PcmSubformat_SInt24_Be:
+        traits.id = PcmSubformat_SInt24_Be;
+        traits.name = "s24_be";
         traits.bit_width = 24;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5325,13 +5377,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt24_Be;
-        traits.default_variant = PcmFormat_SInt24;
-        traits.be_variant = PcmFormat_SInt24_Be;
-        traits.le_variant = PcmFormat_SInt24_Le;
+        traits.portable_alias = PcmSubformat_SInt24_Be;
+        traits.default_variant = PcmSubformat_SInt24;
+        traits.be_variant = PcmSubformat_SInt24_Be;
+        traits.le_variant = PcmSubformat_SInt24_Le;
         break;
 
-    case PcmFormat_SInt24_Le:
+    case PcmSubformat_SInt24_Le:
+        traits.id = PcmSubformat_SInt24_Le;
+        traits.name = "s24_le";
         traits.bit_width = 24;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5340,29 +5394,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt24_Le;
-        traits.default_variant = PcmFormat_SInt24;
-        traits.be_variant = PcmFormat_SInt24_Be;
-        traits.le_variant = PcmFormat_SInt24_Le;
+        traits.portable_alias = PcmSubformat_SInt24_Le;
+        traits.default_variant = PcmSubformat_SInt24;
+        traits.be_variant = PcmSubformat_SInt24_Be;
+        traits.le_variant = PcmSubformat_SInt24_Le;
         break;
 
-    case PcmFormat_UInt24:
+    case PcmSubformat_UInt24:
+        traits.id = PcmSubformat_UInt24;
+        traits.name = "u24";
         traits.bit_width = 24;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt24_Be;
+        traits.portable_alias = PcmSubformat_UInt24_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt24_Le;
+        traits.portable_alias = PcmSubformat_UInt24_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt24;
-        traits.be_variant = PcmFormat_UInt24_Be;
-        traits.le_variant = PcmFormat_UInt24_Le;
+        traits.default_variant = PcmSubformat_UInt24;
+        traits.be_variant = PcmSubformat_UInt24_Be;
+        traits.le_variant = PcmSubformat_UInt24_Le;
         break;
 
-    case PcmFormat_UInt24_Be:
+    case PcmSubformat_UInt24_Be:
+        traits.id = PcmSubformat_UInt24_Be;
+        traits.name = "u24_be";
         traits.bit_width = 24;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -5371,13 +5429,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt24_Be;
-        traits.default_variant = PcmFormat_UInt24;
-        traits.be_variant = PcmFormat_UInt24_Be;
-        traits.le_variant = PcmFormat_UInt24_Le;
+        traits.portable_alias = PcmSubformat_UInt24_Be;
+        traits.default_variant = PcmSubformat_UInt24;
+        traits.be_variant = PcmSubformat_UInt24_Be;
+        traits.le_variant = PcmSubformat_UInt24_Le;
         break;
 
-    case PcmFormat_UInt24_Le:
+    case PcmSubformat_UInt24_Le:
+        traits.id = PcmSubformat_UInt24_Le;
+        traits.name = "u24_le";
         traits.bit_width = 24;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -5386,29 +5446,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt24_Le;
-        traits.default_variant = PcmFormat_UInt24;
-        traits.be_variant = PcmFormat_UInt24_Be;
-        traits.le_variant = PcmFormat_UInt24_Le;
+        traits.portable_alias = PcmSubformat_UInt24_Le;
+        traits.default_variant = PcmSubformat_UInt24;
+        traits.be_variant = PcmSubformat_UInt24_Be;
+        traits.le_variant = PcmSubformat_UInt24_Le;
         break;
 
-    case PcmFormat_SInt24_4:
+    case PcmSubformat_SInt24_4:
+        traits.id = PcmSubformat_SInt24_4;
+        traits.name = "s24_4";
         traits.bit_width = 32;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt24_4_Be;
+        traits.portable_alias = PcmSubformat_SInt24_4_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt24_4_Le;
+        traits.portable_alias = PcmSubformat_SInt24_4_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt24_4;
-        traits.be_variant = PcmFormat_SInt24_4_Be;
-        traits.le_variant = PcmFormat_SInt24_4_Le;
+        traits.default_variant = PcmSubformat_SInt24_4;
+        traits.be_variant = PcmSubformat_SInt24_4_Be;
+        traits.le_variant = PcmSubformat_SInt24_4_Le;
         break;
 
-    case PcmFormat_SInt24_4_Be:
+    case PcmSubformat_SInt24_4_Be:
+        traits.id = PcmSubformat_SInt24_4_Be;
+        traits.name = "s24_4be";
         traits.bit_width = 32;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsAligned;
@@ -5417,13 +5481,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt24_4_Be;
-        traits.default_variant = PcmFormat_SInt24_4;
-        traits.be_variant = PcmFormat_SInt24_4_Be;
-        traits.le_variant = PcmFormat_SInt24_4_Le;
+        traits.portable_alias = PcmSubformat_SInt24_4_Be;
+        traits.default_variant = PcmSubformat_SInt24_4;
+        traits.be_variant = PcmSubformat_SInt24_4_Be;
+        traits.le_variant = PcmSubformat_SInt24_4_Le;
         break;
 
-    case PcmFormat_SInt24_4_Le:
+    case PcmSubformat_SInt24_4_Le:
+        traits.id = PcmSubformat_SInt24_4_Le;
+        traits.name = "s24_4le";
         traits.bit_width = 32;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsAligned;
@@ -5432,29 +5498,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt24_4_Le;
-        traits.default_variant = PcmFormat_SInt24_4;
-        traits.be_variant = PcmFormat_SInt24_4_Be;
-        traits.le_variant = PcmFormat_SInt24_4_Le;
+        traits.portable_alias = PcmSubformat_SInt24_4_Le;
+        traits.default_variant = PcmSubformat_SInt24_4;
+        traits.be_variant = PcmSubformat_SInt24_4_Be;
+        traits.le_variant = PcmSubformat_SInt24_4_Le;
         break;
 
-    case PcmFormat_UInt24_4:
+    case PcmSubformat_UInt24_4:
+        traits.id = PcmSubformat_UInt24_4;
+        traits.name = "u24_4";
         traits.bit_width = 32;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt24_4_Be;
+        traits.portable_alias = PcmSubformat_UInt24_4_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt24_4_Le;
+        traits.portable_alias = PcmSubformat_UInt24_4_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt24_4;
-        traits.be_variant = PcmFormat_UInt24_4_Be;
-        traits.le_variant = PcmFormat_UInt24_4_Le;
+        traits.default_variant = PcmSubformat_UInt24_4;
+        traits.be_variant = PcmSubformat_UInt24_4_Be;
+        traits.le_variant = PcmSubformat_UInt24_4_Le;
         break;
 
-    case PcmFormat_UInt24_4_Be:
+    case PcmSubformat_UInt24_4_Be:
+        traits.id = PcmSubformat_UInt24_4_Be;
+        traits.name = "u24_4be";
         traits.bit_width = 32;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsAligned;
@@ -5463,13 +5533,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt24_4_Be;
-        traits.default_variant = PcmFormat_UInt24_4;
-        traits.be_variant = PcmFormat_UInt24_4_Be;
-        traits.le_variant = PcmFormat_UInt24_4_Le;
+        traits.portable_alias = PcmSubformat_UInt24_4_Be;
+        traits.default_variant = PcmSubformat_UInt24_4;
+        traits.be_variant = PcmSubformat_UInt24_4_Be;
+        traits.le_variant = PcmSubformat_UInt24_4_Le;
         break;
 
-    case PcmFormat_UInt24_4_Le:
+    case PcmSubformat_UInt24_4_Le:
+        traits.id = PcmSubformat_UInt24_4_Le;
+        traits.name = "u24_4le";
         traits.bit_width = 32;
         traits.bit_depth = 24;
         traits.flags = Pcm_IsInteger | Pcm_IsAligned;
@@ -5478,29 +5550,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt24_4_Le;
-        traits.default_variant = PcmFormat_UInt24_4;
-        traits.be_variant = PcmFormat_UInt24_4_Be;
-        traits.le_variant = PcmFormat_UInt24_4_Le;
+        traits.portable_alias = PcmSubformat_UInt24_4_Le;
+        traits.default_variant = PcmSubformat_UInt24_4;
+        traits.be_variant = PcmSubformat_UInt24_4_Be;
+        traits.le_variant = PcmSubformat_UInt24_4_Le;
         break;
 
-    case PcmFormat_SInt32:
+    case PcmSubformat_SInt32:
+        traits.id = PcmSubformat_SInt32;
+        traits.name = "s32";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt32_Be;
+        traits.portable_alias = PcmSubformat_SInt32_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt32_Le;
+        traits.portable_alias = PcmSubformat_SInt32_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt32;
-        traits.be_variant = PcmFormat_SInt32_Be;
-        traits.le_variant = PcmFormat_SInt32_Le;
+        traits.default_variant = PcmSubformat_SInt32;
+        traits.be_variant = PcmSubformat_SInt32_Be;
+        traits.le_variant = PcmSubformat_SInt32_Le;
         break;
 
-    case PcmFormat_SInt32_Be:
+    case PcmSubformat_SInt32_Be:
+        traits.id = PcmSubformat_SInt32_Be;
+        traits.name = "s32_be";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5509,13 +5585,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt32_Be;
-        traits.default_variant = PcmFormat_SInt32;
-        traits.be_variant = PcmFormat_SInt32_Be;
-        traits.le_variant = PcmFormat_SInt32_Le;
+        traits.portable_alias = PcmSubformat_SInt32_Be;
+        traits.default_variant = PcmSubformat_SInt32;
+        traits.be_variant = PcmSubformat_SInt32_Be;
+        traits.le_variant = PcmSubformat_SInt32_Le;
         break;
 
-    case PcmFormat_SInt32_Le:
+    case PcmSubformat_SInt32_Le:
+        traits.id = PcmSubformat_SInt32_Le;
+        traits.name = "s32_le";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5524,29 +5602,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt32_Le;
-        traits.default_variant = PcmFormat_SInt32;
-        traits.be_variant = PcmFormat_SInt32_Be;
-        traits.le_variant = PcmFormat_SInt32_Le;
+        traits.portable_alias = PcmSubformat_SInt32_Le;
+        traits.default_variant = PcmSubformat_SInt32;
+        traits.be_variant = PcmSubformat_SInt32_Be;
+        traits.le_variant = PcmSubformat_SInt32_Le;
         break;
 
-    case PcmFormat_UInt32:
+    case PcmSubformat_UInt32:
+        traits.id = PcmSubformat_UInt32;
+        traits.name = "u32";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt32_Be;
+        traits.portable_alias = PcmSubformat_UInt32_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt32_Le;
+        traits.portable_alias = PcmSubformat_UInt32_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt32;
-        traits.be_variant = PcmFormat_UInt32_Be;
-        traits.le_variant = PcmFormat_UInt32_Le;
+        traits.default_variant = PcmSubformat_UInt32;
+        traits.be_variant = PcmSubformat_UInt32_Be;
+        traits.le_variant = PcmSubformat_UInt32_Le;
         break;
 
-    case PcmFormat_UInt32_Be:
+    case PcmSubformat_UInt32_Be:
+        traits.id = PcmSubformat_UInt32_Be;
+        traits.name = "u32_be";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -5555,13 +5637,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt32_Be;
-        traits.default_variant = PcmFormat_UInt32;
-        traits.be_variant = PcmFormat_UInt32_Be;
-        traits.le_variant = PcmFormat_UInt32_Le;
+        traits.portable_alias = PcmSubformat_UInt32_Be;
+        traits.default_variant = PcmSubformat_UInt32;
+        traits.be_variant = PcmSubformat_UInt32_Be;
+        traits.le_variant = PcmSubformat_UInt32_Le;
         break;
 
-    case PcmFormat_UInt32_Le:
+    case PcmSubformat_UInt32_Le:
+        traits.id = PcmSubformat_UInt32_Le;
+        traits.name = "u32_le";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -5570,29 +5654,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt32_Le;
-        traits.default_variant = PcmFormat_UInt32;
-        traits.be_variant = PcmFormat_UInt32_Be;
-        traits.le_variant = PcmFormat_UInt32_Le;
+        traits.portable_alias = PcmSubformat_UInt32_Le;
+        traits.default_variant = PcmSubformat_UInt32;
+        traits.be_variant = PcmSubformat_UInt32_Be;
+        traits.le_variant = PcmSubformat_UInt32_Le;
         break;
 
-    case PcmFormat_SInt64:
+    case PcmSubformat_SInt64:
+        traits.id = PcmSubformat_SInt64;
+        traits.name = "s64";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_SInt64_Be;
+        traits.portable_alias = PcmSubformat_SInt64_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_SInt64_Le;
+        traits.portable_alias = PcmSubformat_SInt64_Le;
 #endif
-        traits.default_variant = PcmFormat_SInt64;
-        traits.be_variant = PcmFormat_SInt64_Be;
-        traits.le_variant = PcmFormat_SInt64_Le;
+        traits.default_variant = PcmSubformat_SInt64;
+        traits.be_variant = PcmSubformat_SInt64_Be;
+        traits.le_variant = PcmSubformat_SInt64_Le;
         break;
 
-    case PcmFormat_SInt64_Be:
+    case PcmSubformat_SInt64_Be:
+        traits.id = PcmSubformat_SInt64_Be;
+        traits.name = "s64_be";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5601,13 +5689,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_SInt64_Be;
-        traits.default_variant = PcmFormat_SInt64;
-        traits.be_variant = PcmFormat_SInt64_Be;
-        traits.le_variant = PcmFormat_SInt64_Le;
+        traits.portable_alias = PcmSubformat_SInt64_Be;
+        traits.default_variant = PcmSubformat_SInt64;
+        traits.be_variant = PcmSubformat_SInt64_Be;
+        traits.le_variant = PcmSubformat_SInt64_Le;
         break;
 
-    case PcmFormat_SInt64_Le:
+    case PcmSubformat_SInt64_Le:
+        traits.id = PcmSubformat_SInt64_Le;
+        traits.name = "s64_le";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsInteger | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5616,29 +5706,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_SInt64_Le;
-        traits.default_variant = PcmFormat_SInt64;
-        traits.be_variant = PcmFormat_SInt64_Be;
-        traits.le_variant = PcmFormat_SInt64_Le;
+        traits.portable_alias = PcmSubformat_SInt64_Le;
+        traits.default_variant = PcmSubformat_SInt64;
+        traits.be_variant = PcmSubformat_SInt64_Be;
+        traits.le_variant = PcmSubformat_SInt64_Le;
         break;
 
-    case PcmFormat_UInt64:
+    case PcmSubformat_UInt64:
+        traits.id = PcmSubformat_UInt64;
+        traits.name = "u64";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_UInt64_Be;
+        traits.portable_alias = PcmSubformat_UInt64_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_UInt64_Le;
+        traits.portable_alias = PcmSubformat_UInt64_Le;
 #endif
-        traits.default_variant = PcmFormat_UInt64;
-        traits.be_variant = PcmFormat_UInt64_Be;
-        traits.le_variant = PcmFormat_UInt64_Le;
+        traits.default_variant = PcmSubformat_UInt64;
+        traits.be_variant = PcmSubformat_UInt64_Be;
+        traits.le_variant = PcmSubformat_UInt64_Le;
         break;
 
-    case PcmFormat_UInt64_Be:
+    case PcmSubformat_UInt64_Be:
+        traits.id = PcmSubformat_UInt64_Be;
+        traits.name = "u64_be";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -5647,13 +5741,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_UInt64_Be;
-        traits.default_variant = PcmFormat_UInt64;
-        traits.be_variant = PcmFormat_UInt64_Be;
-        traits.le_variant = PcmFormat_UInt64_Le;
+        traits.portable_alias = PcmSubformat_UInt64_Be;
+        traits.default_variant = PcmSubformat_UInt64;
+        traits.be_variant = PcmSubformat_UInt64_Be;
+        traits.le_variant = PcmSubformat_UInt64_Le;
         break;
 
-    case PcmFormat_UInt64_Le:
+    case PcmSubformat_UInt64_Le:
+        traits.id = PcmSubformat_UInt64_Le;
+        traits.name = "u64_le";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsInteger | Pcm_IsPacked | Pcm_IsAligned;
@@ -5662,29 +5758,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_UInt64_Le;
-        traits.default_variant = PcmFormat_UInt64;
-        traits.be_variant = PcmFormat_UInt64_Be;
-        traits.le_variant = PcmFormat_UInt64_Le;
+        traits.portable_alias = PcmSubformat_UInt64_Le;
+        traits.default_variant = PcmSubformat_UInt64;
+        traits.be_variant = PcmSubformat_UInt64_Be;
+        traits.le_variant = PcmSubformat_UInt64_Le;
         break;
 
-    case PcmFormat_Float32:
+    case PcmSubformat_Float32:
+        traits.id = PcmSubformat_Float32;
+        traits.name = "f32";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsFloat | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_Float32_Be;
+        traits.portable_alias = PcmSubformat_Float32_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_Float32_Le;
+        traits.portable_alias = PcmSubformat_Float32_Le;
 #endif
-        traits.default_variant = PcmFormat_Float32;
-        traits.be_variant = PcmFormat_Float32_Be;
-        traits.le_variant = PcmFormat_Float32_Le;
+        traits.default_variant = PcmSubformat_Float32;
+        traits.be_variant = PcmSubformat_Float32_Be;
+        traits.le_variant = PcmSubformat_Float32_Le;
         break;
 
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32_Be:
+        traits.id = PcmSubformat_Float32_Be;
+        traits.name = "f32_be";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsFloat | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5693,13 +5793,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_Float32_Be;
-        traits.default_variant = PcmFormat_Float32;
-        traits.be_variant = PcmFormat_Float32_Be;
-        traits.le_variant = PcmFormat_Float32_Le;
+        traits.portable_alias = PcmSubformat_Float32_Be;
+        traits.default_variant = PcmSubformat_Float32;
+        traits.be_variant = PcmSubformat_Float32_Be;
+        traits.le_variant = PcmSubformat_Float32_Le;
         break;
 
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32_Le:
+        traits.id = PcmSubformat_Float32_Le;
+        traits.name = "f32_le";
         traits.bit_width = 32;
         traits.bit_depth = 32;
         traits.flags = Pcm_IsFloat | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5708,29 +5810,33 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_Float32_Le;
-        traits.default_variant = PcmFormat_Float32;
-        traits.be_variant = PcmFormat_Float32_Be;
-        traits.le_variant = PcmFormat_Float32_Le;
+        traits.portable_alias = PcmSubformat_Float32_Le;
+        traits.default_variant = PcmSubformat_Float32;
+        traits.be_variant = PcmSubformat_Float32_Be;
+        traits.le_variant = PcmSubformat_Float32_Le;
         break;
 
-    case PcmFormat_Float64:
+    case PcmSubformat_Float64:
+        traits.id = PcmSubformat_Float64;
+        traits.name = "f64";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsFloat | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
 #if ROC_CPU_ENDIAN == ROC_CPU_BE
         traits.flags |= Pcm_IsNative | Pcm_IsBig;
-        traits.portable_alias = PcmFormat_Float64_Be;
+        traits.portable_alias = PcmSubformat_Float64_Be;
 #else
         traits.flags |= Pcm_IsNative | Pcm_IsLittle;
-        traits.portable_alias = PcmFormat_Float64_Le;
+        traits.portable_alias = PcmSubformat_Float64_Le;
 #endif
-        traits.default_variant = PcmFormat_Float64;
-        traits.be_variant = PcmFormat_Float64_Be;
-        traits.le_variant = PcmFormat_Float64_Le;
+        traits.default_variant = PcmSubformat_Float64;
+        traits.be_variant = PcmSubformat_Float64_Be;
+        traits.le_variant = PcmSubformat_Float64_Le;
         break;
 
-    case PcmFormat_Float64_Be:
+    case PcmSubformat_Float64_Be:
+        traits.id = PcmSubformat_Float64_Be;
+        traits.name = "f64_be";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsFloat | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5739,13 +5845,15 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsBig;
 #endif
-        traits.portable_alias = PcmFormat_Float64_Be;
-        traits.default_variant = PcmFormat_Float64;
-        traits.be_variant = PcmFormat_Float64_Be;
-        traits.le_variant = PcmFormat_Float64_Le;
+        traits.portable_alias = PcmSubformat_Float64_Be;
+        traits.default_variant = PcmSubformat_Float64;
+        traits.be_variant = PcmSubformat_Float64_Be;
+        traits.le_variant = PcmSubformat_Float64_Le;
         break;
 
-    case PcmFormat_Float64_Le:
+    case PcmSubformat_Float64_Le:
+        traits.id = PcmSubformat_Float64_Le;
+        traits.name = "f64_le";
         traits.bit_width = 64;
         traits.bit_depth = 64;
         traits.flags = Pcm_IsFloat | Pcm_IsSigned | Pcm_IsPacked | Pcm_IsAligned;
@@ -5754,10 +5862,10 @@ PcmTraits pcm_format_traits(PcmFormat format) {
 #else
         traits.flags |= Pcm_IsLittle;
 #endif
-        traits.portable_alias = PcmFormat_Float64_Le;
-        traits.default_variant = PcmFormat_Float64;
-        traits.be_variant = PcmFormat_Float64_Be;
-        traits.le_variant = PcmFormat_Float64_Le;
+        traits.portable_alias = PcmSubformat_Float64_Le;
+        traits.default_variant = PcmSubformat_Float64;
+        traits.be_variant = PcmSubformat_Float64_Be;
+        traits.le_variant = PcmSubformat_Float64_Le;
         break;
 
     default:
@@ -5767,163 +5875,163 @@ PcmTraits pcm_format_traits(PcmFormat format) {
     return traits;
 }
 
-const char* pcm_format_to_str(PcmFormat format) {
+const char* pcm_subformat_to_str(PcmSubformat format) {
     switch (format) {
-    case PcmFormat_SInt8:
+    case PcmSubformat_SInt8:
         return "s8";
-    case PcmFormat_SInt8_Be:
+    case PcmSubformat_SInt8_Be:
         return "s8_be";
-    case PcmFormat_SInt8_Le:
+    case PcmSubformat_SInt8_Le:
         return "s8_le";
-    case PcmFormat_UInt8:
+    case PcmSubformat_UInt8:
         return "u8";
-    case PcmFormat_UInt8_Be:
+    case PcmSubformat_UInt8_Be:
         return "u8_be";
-    case PcmFormat_UInt8_Le:
+    case PcmSubformat_UInt8_Le:
         return "u8_le";
-    case PcmFormat_SInt16:
+    case PcmSubformat_SInt16:
         return "s16";
-    case PcmFormat_SInt16_Be:
+    case PcmSubformat_SInt16_Be:
         return "s16_be";
-    case PcmFormat_SInt16_Le:
+    case PcmSubformat_SInt16_Le:
         return "s16_le";
-    case PcmFormat_UInt16:
+    case PcmSubformat_UInt16:
         return "u16";
-    case PcmFormat_UInt16_Be:
+    case PcmSubformat_UInt16_Be:
         return "u16_be";
-    case PcmFormat_UInt16_Le:
+    case PcmSubformat_UInt16_Le:
         return "u16_le";
-    case PcmFormat_SInt18:
+    case PcmSubformat_SInt18:
         return "s18";
-    case PcmFormat_SInt18_Be:
+    case PcmSubformat_SInt18_Be:
         return "s18_be";
-    case PcmFormat_SInt18_Le:
+    case PcmSubformat_SInt18_Le:
         return "s18_le";
-    case PcmFormat_UInt18:
+    case PcmSubformat_UInt18:
         return "u18";
-    case PcmFormat_UInt18_Be:
+    case PcmSubformat_UInt18_Be:
         return "u18_be";
-    case PcmFormat_UInt18_Le:
+    case PcmSubformat_UInt18_Le:
         return "u18_le";
-    case PcmFormat_SInt18_3:
+    case PcmSubformat_SInt18_3:
         return "s18_3";
-    case PcmFormat_SInt18_3_Be:
+    case PcmSubformat_SInt18_3_Be:
         return "s18_3be";
-    case PcmFormat_SInt18_3_Le:
+    case PcmSubformat_SInt18_3_Le:
         return "s18_3le";
-    case PcmFormat_UInt18_3:
+    case PcmSubformat_UInt18_3:
         return "u18_3";
-    case PcmFormat_UInt18_3_Be:
+    case PcmSubformat_UInt18_3_Be:
         return "u18_3be";
-    case PcmFormat_UInt18_3_Le:
+    case PcmSubformat_UInt18_3_Le:
         return "u18_3le";
-    case PcmFormat_SInt18_4:
+    case PcmSubformat_SInt18_4:
         return "s18_4";
-    case PcmFormat_SInt18_4_Be:
+    case PcmSubformat_SInt18_4_Be:
         return "s18_4be";
-    case PcmFormat_SInt18_4_Le:
+    case PcmSubformat_SInt18_4_Le:
         return "s18_4le";
-    case PcmFormat_UInt18_4:
+    case PcmSubformat_UInt18_4:
         return "u18_4";
-    case PcmFormat_UInt18_4_Be:
+    case PcmSubformat_UInt18_4_Be:
         return "u18_4be";
-    case PcmFormat_UInt18_4_Le:
+    case PcmSubformat_UInt18_4_Le:
         return "u18_4le";
-    case PcmFormat_SInt20:
+    case PcmSubformat_SInt20:
         return "s20";
-    case PcmFormat_SInt20_Be:
+    case PcmSubformat_SInt20_Be:
         return "s20_be";
-    case PcmFormat_SInt20_Le:
+    case PcmSubformat_SInt20_Le:
         return "s20_le";
-    case PcmFormat_UInt20:
+    case PcmSubformat_UInt20:
         return "u20";
-    case PcmFormat_UInt20_Be:
+    case PcmSubformat_UInt20_Be:
         return "u20_be";
-    case PcmFormat_UInt20_Le:
+    case PcmSubformat_UInt20_Le:
         return "u20_le";
-    case PcmFormat_SInt20_3:
+    case PcmSubformat_SInt20_3:
         return "s20_3";
-    case PcmFormat_SInt20_3_Be:
+    case PcmSubformat_SInt20_3_Be:
         return "s20_3be";
-    case PcmFormat_SInt20_3_Le:
+    case PcmSubformat_SInt20_3_Le:
         return "s20_3le";
-    case PcmFormat_UInt20_3:
+    case PcmSubformat_UInt20_3:
         return "u20_3";
-    case PcmFormat_UInt20_3_Be:
+    case PcmSubformat_UInt20_3_Be:
         return "u20_3be";
-    case PcmFormat_UInt20_3_Le:
+    case PcmSubformat_UInt20_3_Le:
         return "u20_3le";
-    case PcmFormat_SInt20_4:
+    case PcmSubformat_SInt20_4:
         return "s20_4";
-    case PcmFormat_SInt20_4_Be:
+    case PcmSubformat_SInt20_4_Be:
         return "s20_4be";
-    case PcmFormat_SInt20_4_Le:
+    case PcmSubformat_SInt20_4_Le:
         return "s20_4le";
-    case PcmFormat_UInt20_4:
+    case PcmSubformat_UInt20_4:
         return "u20_4";
-    case PcmFormat_UInt20_4_Be:
+    case PcmSubformat_UInt20_4_Be:
         return "u20_4be";
-    case PcmFormat_UInt20_4_Le:
+    case PcmSubformat_UInt20_4_Le:
         return "u20_4le";
-    case PcmFormat_SInt24:
+    case PcmSubformat_SInt24:
         return "s24";
-    case PcmFormat_SInt24_Be:
+    case PcmSubformat_SInt24_Be:
         return "s24_be";
-    case PcmFormat_SInt24_Le:
+    case PcmSubformat_SInt24_Le:
         return "s24_le";
-    case PcmFormat_UInt24:
+    case PcmSubformat_UInt24:
         return "u24";
-    case PcmFormat_UInt24_Be:
+    case PcmSubformat_UInt24_Be:
         return "u24_be";
-    case PcmFormat_UInt24_Le:
+    case PcmSubformat_UInt24_Le:
         return "u24_le";
-    case PcmFormat_SInt24_4:
+    case PcmSubformat_SInt24_4:
         return "s24_4";
-    case PcmFormat_SInt24_4_Be:
+    case PcmSubformat_SInt24_4_Be:
         return "s24_4be";
-    case PcmFormat_SInt24_4_Le:
+    case PcmSubformat_SInt24_4_Le:
         return "s24_4le";
-    case PcmFormat_UInt24_4:
+    case PcmSubformat_UInt24_4:
         return "u24_4";
-    case PcmFormat_UInt24_4_Be:
+    case PcmSubformat_UInt24_4_Be:
         return "u24_4be";
-    case PcmFormat_UInt24_4_Le:
+    case PcmSubformat_UInt24_4_Le:
         return "u24_4le";
-    case PcmFormat_SInt32:
+    case PcmSubformat_SInt32:
         return "s32";
-    case PcmFormat_SInt32_Be:
+    case PcmSubformat_SInt32_Be:
         return "s32_be";
-    case PcmFormat_SInt32_Le:
+    case PcmSubformat_SInt32_Le:
         return "s32_le";
-    case PcmFormat_UInt32:
+    case PcmSubformat_UInt32:
         return "u32";
-    case PcmFormat_UInt32_Be:
+    case PcmSubformat_UInt32_Be:
         return "u32_be";
-    case PcmFormat_UInt32_Le:
+    case PcmSubformat_UInt32_Le:
         return "u32_le";
-    case PcmFormat_SInt64:
+    case PcmSubformat_SInt64:
         return "s64";
-    case PcmFormat_SInt64_Be:
+    case PcmSubformat_SInt64_Be:
         return "s64_be";
-    case PcmFormat_SInt64_Le:
+    case PcmSubformat_SInt64_Le:
         return "s64_le";
-    case PcmFormat_UInt64:
+    case PcmSubformat_UInt64:
         return "u64";
-    case PcmFormat_UInt64_Be:
+    case PcmSubformat_UInt64_Be:
         return "u64_be";
-    case PcmFormat_UInt64_Le:
+    case PcmSubformat_UInt64_Le:
         return "u64_le";
-    case PcmFormat_Float32:
+    case PcmSubformat_Float32:
         return "f32";
-    case PcmFormat_Float32_Be:
+    case PcmSubformat_Float32_Be:
         return "f32_be";
-    case PcmFormat_Float32_Le:
+    case PcmSubformat_Float32_Le:
         return "f32_le";
-    case PcmFormat_Float64:
+    case PcmSubformat_Float64:
         return "f64";
-    case PcmFormat_Float64_Be:
+    case PcmSubformat_Float64_Be:
         return "f64_be";
-    case PcmFormat_Float64_Le:
+    case PcmSubformat_Float64_Le:
         return "f64_le";
     default:
         break;
@@ -5931,332 +6039,332 @@ const char* pcm_format_to_str(PcmFormat format) {
     return NULL;
 }
 
-PcmFormat pcm_format_from_str(const char* str) {
+PcmSubformat pcm_subformat_from_str(const char* str) {
     if (!str) {
-        return PcmFormat_Invalid;
+        return PcmSubformat_Invalid;
     }
     if (str[0] == 'f') {
         if (str[1] == '3') {
             if (str[2] == '2') {
                 if (strcmp(str, "f32") == 0) {
-                    return PcmFormat_Float32;
+                    return PcmSubformat_Float32;
                 }
                 if (strcmp(str, "f32_be") == 0) {
-                    return PcmFormat_Float32_Be;
+                    return PcmSubformat_Float32_Be;
                 }
                 if (strcmp(str, "f32_le") == 0) {
-                    return PcmFormat_Float32_Le;
+                    return PcmSubformat_Float32_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '6') {
             if (str[2] == '4') {
                 if (strcmp(str, "f64") == 0) {
-                    return PcmFormat_Float64;
+                    return PcmSubformat_Float64;
                 }
                 if (strcmp(str, "f64_be") == 0) {
-                    return PcmFormat_Float64_Be;
+                    return PcmSubformat_Float64_Be;
                 }
                 if (strcmp(str, "f64_le") == 0) {
-                    return PcmFormat_Float64_Le;
+                    return PcmSubformat_Float64_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
-        return PcmFormat_Invalid;
+        return PcmSubformat_Invalid;
     }
     if (str[0] == 's') {
         if (str[1] == '1') {
             if (str[2] == '6') {
                 if (strcmp(str, "s16") == 0) {
-                    return PcmFormat_SInt16;
+                    return PcmSubformat_SInt16;
                 }
                 if (strcmp(str, "s16_be") == 0) {
-                    return PcmFormat_SInt16_Be;
+                    return PcmSubformat_SInt16_Be;
                 }
                 if (strcmp(str, "s16_le") == 0) {
-                    return PcmFormat_SInt16_Le;
+                    return PcmSubformat_SInt16_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
             if (str[2] == '8') {
                 if (strcmp(str, "s18") == 0) {
-                    return PcmFormat_SInt18;
+                    return PcmSubformat_SInt18;
                 }
                 if (strcmp(str, "s18_be") == 0) {
-                    return PcmFormat_SInt18_Be;
+                    return PcmSubformat_SInt18_Be;
                 }
                 if (strcmp(str, "s18_le") == 0) {
-                    return PcmFormat_SInt18_Le;
+                    return PcmSubformat_SInt18_Le;
                 }
                 if (strcmp(str, "s18_3") == 0) {
-                    return PcmFormat_SInt18_3;
+                    return PcmSubformat_SInt18_3;
                 }
                 if (strcmp(str, "s18_3be") == 0) {
-                    return PcmFormat_SInt18_3_Be;
+                    return PcmSubformat_SInt18_3_Be;
                 }
                 if (strcmp(str, "s18_3le") == 0) {
-                    return PcmFormat_SInt18_3_Le;
+                    return PcmSubformat_SInt18_3_Le;
                 }
                 if (strcmp(str, "s18_4") == 0) {
-                    return PcmFormat_SInt18_4;
+                    return PcmSubformat_SInt18_4;
                 }
                 if (strcmp(str, "s18_4be") == 0) {
-                    return PcmFormat_SInt18_4_Be;
+                    return PcmSubformat_SInt18_4_Be;
                 }
                 if (strcmp(str, "s18_4le") == 0) {
-                    return PcmFormat_SInt18_4_Le;
+                    return PcmSubformat_SInt18_4_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '2') {
             if (str[2] == '0') {
                 if (strcmp(str, "s20") == 0) {
-                    return PcmFormat_SInt20;
+                    return PcmSubformat_SInt20;
                 }
                 if (strcmp(str, "s20_be") == 0) {
-                    return PcmFormat_SInt20_Be;
+                    return PcmSubformat_SInt20_Be;
                 }
                 if (strcmp(str, "s20_le") == 0) {
-                    return PcmFormat_SInt20_Le;
+                    return PcmSubformat_SInt20_Le;
                 }
                 if (strcmp(str, "s20_3") == 0) {
-                    return PcmFormat_SInt20_3;
+                    return PcmSubformat_SInt20_3;
                 }
                 if (strcmp(str, "s20_3be") == 0) {
-                    return PcmFormat_SInt20_3_Be;
+                    return PcmSubformat_SInt20_3_Be;
                 }
                 if (strcmp(str, "s20_3le") == 0) {
-                    return PcmFormat_SInt20_3_Le;
+                    return PcmSubformat_SInt20_3_Le;
                 }
                 if (strcmp(str, "s20_4") == 0) {
-                    return PcmFormat_SInt20_4;
+                    return PcmSubformat_SInt20_4;
                 }
                 if (strcmp(str, "s20_4be") == 0) {
-                    return PcmFormat_SInt20_4_Be;
+                    return PcmSubformat_SInt20_4_Be;
                 }
                 if (strcmp(str, "s20_4le") == 0) {
-                    return PcmFormat_SInt20_4_Le;
+                    return PcmSubformat_SInt20_4_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
             if (str[2] == '4') {
                 if (strcmp(str, "s24") == 0) {
-                    return PcmFormat_SInt24;
+                    return PcmSubformat_SInt24;
                 }
                 if (strcmp(str, "s24_be") == 0) {
-                    return PcmFormat_SInt24_Be;
+                    return PcmSubformat_SInt24_Be;
                 }
                 if (strcmp(str, "s24_le") == 0) {
-                    return PcmFormat_SInt24_Le;
+                    return PcmSubformat_SInt24_Le;
                 }
                 if (strcmp(str, "s24_4") == 0) {
-                    return PcmFormat_SInt24_4;
+                    return PcmSubformat_SInt24_4;
                 }
                 if (strcmp(str, "s24_4be") == 0) {
-                    return PcmFormat_SInt24_4_Be;
+                    return PcmSubformat_SInt24_4_Be;
                 }
                 if (strcmp(str, "s24_4le") == 0) {
-                    return PcmFormat_SInt24_4_Le;
+                    return PcmSubformat_SInt24_4_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '3') {
             if (str[2] == '2') {
                 if (strcmp(str, "s32") == 0) {
-                    return PcmFormat_SInt32;
+                    return PcmSubformat_SInt32;
                 }
                 if (strcmp(str, "s32_be") == 0) {
-                    return PcmFormat_SInt32_Be;
+                    return PcmSubformat_SInt32_Be;
                 }
                 if (strcmp(str, "s32_le") == 0) {
-                    return PcmFormat_SInt32_Le;
+                    return PcmSubformat_SInt32_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '6') {
             if (str[2] == '4') {
                 if (strcmp(str, "s64") == 0) {
-                    return PcmFormat_SInt64;
+                    return PcmSubformat_SInt64;
                 }
                 if (strcmp(str, "s64_be") == 0) {
-                    return PcmFormat_SInt64_Be;
+                    return PcmSubformat_SInt64_Be;
                 }
                 if (strcmp(str, "s64_le") == 0) {
-                    return PcmFormat_SInt64_Le;
+                    return PcmSubformat_SInt64_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '8') {
             if (strcmp(str, "s8") == 0) {
-                return PcmFormat_SInt8;
+                return PcmSubformat_SInt8;
             }
             if (strcmp(str, "s8_be") == 0) {
-                return PcmFormat_SInt8_Be;
+                return PcmSubformat_SInt8_Be;
             }
             if (strcmp(str, "s8_le") == 0) {
-                return PcmFormat_SInt8_Le;
+                return PcmSubformat_SInt8_Le;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
-        return PcmFormat_Invalid;
+        return PcmSubformat_Invalid;
     }
     if (str[0] == 'u') {
         if (str[1] == '1') {
             if (str[2] == '6') {
                 if (strcmp(str, "u16") == 0) {
-                    return PcmFormat_UInt16;
+                    return PcmSubformat_UInt16;
                 }
                 if (strcmp(str, "u16_be") == 0) {
-                    return PcmFormat_UInt16_Be;
+                    return PcmSubformat_UInt16_Be;
                 }
                 if (strcmp(str, "u16_le") == 0) {
-                    return PcmFormat_UInt16_Le;
+                    return PcmSubformat_UInt16_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
             if (str[2] == '8') {
                 if (strcmp(str, "u18") == 0) {
-                    return PcmFormat_UInt18;
+                    return PcmSubformat_UInt18;
                 }
                 if (strcmp(str, "u18_be") == 0) {
-                    return PcmFormat_UInt18_Be;
+                    return PcmSubformat_UInt18_Be;
                 }
                 if (strcmp(str, "u18_le") == 0) {
-                    return PcmFormat_UInt18_Le;
+                    return PcmSubformat_UInt18_Le;
                 }
                 if (strcmp(str, "u18_3") == 0) {
-                    return PcmFormat_UInt18_3;
+                    return PcmSubformat_UInt18_3;
                 }
                 if (strcmp(str, "u18_3be") == 0) {
-                    return PcmFormat_UInt18_3_Be;
+                    return PcmSubformat_UInt18_3_Be;
                 }
                 if (strcmp(str, "u18_3le") == 0) {
-                    return PcmFormat_UInt18_3_Le;
+                    return PcmSubformat_UInt18_3_Le;
                 }
                 if (strcmp(str, "u18_4") == 0) {
-                    return PcmFormat_UInt18_4;
+                    return PcmSubformat_UInt18_4;
                 }
                 if (strcmp(str, "u18_4be") == 0) {
-                    return PcmFormat_UInt18_4_Be;
+                    return PcmSubformat_UInt18_4_Be;
                 }
                 if (strcmp(str, "u18_4le") == 0) {
-                    return PcmFormat_UInt18_4_Le;
+                    return PcmSubformat_UInt18_4_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '2') {
             if (str[2] == '0') {
                 if (strcmp(str, "u20") == 0) {
-                    return PcmFormat_UInt20;
+                    return PcmSubformat_UInt20;
                 }
                 if (strcmp(str, "u20_be") == 0) {
-                    return PcmFormat_UInt20_Be;
+                    return PcmSubformat_UInt20_Be;
                 }
                 if (strcmp(str, "u20_le") == 0) {
-                    return PcmFormat_UInt20_Le;
+                    return PcmSubformat_UInt20_Le;
                 }
                 if (strcmp(str, "u20_3") == 0) {
-                    return PcmFormat_UInt20_3;
+                    return PcmSubformat_UInt20_3;
                 }
                 if (strcmp(str, "u20_3be") == 0) {
-                    return PcmFormat_UInt20_3_Be;
+                    return PcmSubformat_UInt20_3_Be;
                 }
                 if (strcmp(str, "u20_3le") == 0) {
-                    return PcmFormat_UInt20_3_Le;
+                    return PcmSubformat_UInt20_3_Le;
                 }
                 if (strcmp(str, "u20_4") == 0) {
-                    return PcmFormat_UInt20_4;
+                    return PcmSubformat_UInt20_4;
                 }
                 if (strcmp(str, "u20_4be") == 0) {
-                    return PcmFormat_UInt20_4_Be;
+                    return PcmSubformat_UInt20_4_Be;
                 }
                 if (strcmp(str, "u20_4le") == 0) {
-                    return PcmFormat_UInt20_4_Le;
+                    return PcmSubformat_UInt20_4_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
             if (str[2] == '4') {
                 if (strcmp(str, "u24") == 0) {
-                    return PcmFormat_UInt24;
+                    return PcmSubformat_UInt24;
                 }
                 if (strcmp(str, "u24_be") == 0) {
-                    return PcmFormat_UInt24_Be;
+                    return PcmSubformat_UInt24_Be;
                 }
                 if (strcmp(str, "u24_le") == 0) {
-                    return PcmFormat_UInt24_Le;
+                    return PcmSubformat_UInt24_Le;
                 }
                 if (strcmp(str, "u24_4") == 0) {
-                    return PcmFormat_UInt24_4;
+                    return PcmSubformat_UInt24_4;
                 }
                 if (strcmp(str, "u24_4be") == 0) {
-                    return PcmFormat_UInt24_4_Be;
+                    return PcmSubformat_UInt24_4_Be;
                 }
                 if (strcmp(str, "u24_4le") == 0) {
-                    return PcmFormat_UInt24_4_Le;
+                    return PcmSubformat_UInt24_4_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '3') {
             if (str[2] == '2') {
                 if (strcmp(str, "u32") == 0) {
-                    return PcmFormat_UInt32;
+                    return PcmSubformat_UInt32;
                 }
                 if (strcmp(str, "u32_be") == 0) {
-                    return PcmFormat_UInt32_Be;
+                    return PcmSubformat_UInt32_Be;
                 }
                 if (strcmp(str, "u32_le") == 0) {
-                    return PcmFormat_UInt32_Le;
+                    return PcmSubformat_UInt32_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '6') {
             if (str[2] == '4') {
                 if (strcmp(str, "u64") == 0) {
-                    return PcmFormat_UInt64;
+                    return PcmSubformat_UInt64;
                 }
                 if (strcmp(str, "u64_be") == 0) {
-                    return PcmFormat_UInt64_Be;
+                    return PcmSubformat_UInt64_Be;
                 }
                 if (strcmp(str, "u64_le") == 0) {
-                    return PcmFormat_UInt64_Le;
+                    return PcmSubformat_UInt64_Le;
                 }
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
         if (str[1] == '8') {
             if (strcmp(str, "u8") == 0) {
-                return PcmFormat_UInt8;
+                return PcmSubformat_UInt8;
             }
             if (strcmp(str, "u8_be") == 0) {
-                return PcmFormat_UInt8_Be;
+                return PcmSubformat_UInt8_Be;
             }
             if (strcmp(str, "u8_le") == 0) {
-                return PcmFormat_UInt8_Le;
+                return PcmSubformat_UInt8_Le;
             }
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
-        return PcmFormat_Invalid;
+        return PcmSubformat_Invalid;
     }
-    return PcmFormat_Invalid;
+    return PcmSubformat_Invalid;
 }
 
 } // namespace audio
diff --git a/src/internal_modules/roc_audio/pcm_mapper.cpp b/src/internal_modules/roc_audio/pcm_mapper.cpp
index 11727952b..041ffcc7a 100644
--- a/src/internal_modules/roc_audio/pcm_mapper.cpp
+++ b/src/internal_modules/roc_audio/pcm_mapper.cpp
@@ -14,27 +14,27 @@
 namespace roc {
 namespace audio {
 
-PcmMapper::PcmMapper(PcmFormat input_fmt, PcmFormat output_fmt)
+PcmMapper::PcmMapper(PcmSubformat input_fmt, PcmSubformat output_fmt)
     : input_fmt_(input_fmt)
     , output_fmt_(output_fmt)
-    , input_traits_(pcm_format_traits(input_fmt))
-    , output_traits_(pcm_format_traits(output_fmt)) {
+    , input_traits_(pcm_subformat_traits(input_fmt))
+    , output_traits_(pcm_subformat_traits(output_fmt)) {
     // To reduce code size, we generate converters only between raw and non-raw formats.
     // To convert between two non-raw formats, you need a pair of pcm mappers.
-    roc_panic_if_msg(input_fmt != Sample_RawFormat && output_fmt != Sample_RawFormat,
+    roc_panic_if_msg(input_fmt != PcmSubformat_Raw && output_fmt != PcmSubformat_Raw,
                      "pcm mapper: either input or output format must be raw");
 
     // This must not happen if checks above passed.
-    if (!(map_func_ = pcm_format_mapfn(input_fmt, output_fmt))) {
+    if (!(map_func_ = pcm_subformat_mapfn(input_fmt, output_fmt))) {
         roc_panic("pcm mapper: unable to select mapping function");
     }
 }
 
-PcmFormat PcmMapper::input_format() const {
+PcmSubformat PcmMapper::input_format() const {
     return input_fmt_;
 }
 
-PcmFormat PcmMapper::output_format() const {
+PcmSubformat PcmMapper::output_format() const {
     return output_fmt_;
 }
 
diff --git a/src/internal_modules/roc_audio/pcm_mapper.h b/src/internal_modules/roc_audio/pcm_mapper.h
index ebfdb6c23..e6ed5c87a 100644
--- a/src/internal_modules/roc_audio/pcm_mapper.h
+++ b/src/internal_modules/roc_audio/pcm_mapper.h
@@ -12,7 +12,7 @@
 #ifndef ROC_AUDIO_PCM_MAPPER_H_
 #define ROC_AUDIO_PCM_MAPPER_H_
 
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/noncopyable.h"
 #include "roc_core/stddefs.h"
 
@@ -21,17 +21,17 @@ namespace audio {
 
 //! PCM format mapper.
 //! Converts between two PCM formats.
-//! Either input or output format must be raw samples (Sample_RawFormat).
+//! Either input or output format must be raw samples (PcmSubformat_Raw).
 class PcmMapper : public core::NonCopyable<> {
 public:
     //! Initialize.
-    PcmMapper(PcmFormat input_fmt, PcmFormat output_fmt);
+    PcmMapper(PcmSubformat input_fmt, PcmSubformat output_fmt);
 
     //! Get input format.
-    PcmFormat input_format() const;
+    PcmSubformat input_format() const;
 
     //! Get output format.
-    PcmFormat output_format() const;
+    PcmSubformat output_format() const;
 
     //! Get number of input samples (total for all channels) for given number of bytes.
     size_t input_sample_count(size_t input_bytes) const;
@@ -76,8 +76,8 @@ class PcmMapper : public core::NonCopyable<> {
                size_t n_samples);
 
 private:
-    const PcmFormat input_fmt_;
-    const PcmFormat output_fmt_;
+    const PcmSubformat input_fmt_;
+    const PcmSubformat output_fmt_;
 
     const PcmTraits input_traits_;
     const PcmTraits output_traits_;
diff --git a/src/internal_modules/roc_audio/pcm_mapper_reader.cpp b/src/internal_modules/roc_audio/pcm_mapper_reader.cpp
index 57b2fbba2..390c909b9 100644
--- a/src/internal_modules/roc_audio/pcm_mapper_reader.cpp
+++ b/src/internal_modules/roc_audio/pcm_mapper_reader.cpp
@@ -7,7 +7,7 @@
  */
 
 #include "roc_audio/pcm_mapper_reader.h"
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
@@ -25,10 +25,9 @@ PcmMapperReader::PcmMapperReader(IFrameReader& frame_reader,
     , out_spec_(out_spec)
     , num_ch_(out_spec.num_channels())
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid()
-        || in_spec_.sample_format() != SampleFormat_Pcm
-        || out_spec_.sample_format() != SampleFormat_Pcm) {
-        roc_panic("pcm mapper reader: required valid sample specs with pcm format:"
+    if (!in_spec_.is_complete() || !out_spec_.is_complete()
+        || in_spec_.format() != Format_Pcm || out_spec_.format() != Format_Pcm) {
+        roc_panic("pcm mapper reader: required complete sample specs with pcm format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
@@ -49,7 +48,8 @@ PcmMapperReader::PcmMapperReader(IFrameReader& frame_reader,
             sample_spec_to_str(in_spec_).c_str(), sample_spec_to_str(out_spec_).c_str());
     }
 
-    mapper_.reset(new (mapper_) PcmMapper(in_spec_.pcm_format(), out_spec_.pcm_format()));
+    mapper_.reset(new (mapper_)
+                      PcmMapper(in_spec_.pcm_subformat(), out_spec_.pcm_subformat()));
 
     if (mapper_->input_bit_count(1) % 8 != 0 || mapper_->output_bit_count(1) % 8 != 0) {
         roc_panic("pcm mapper reader: unsupported not byte-aligned encoding:"
diff --git a/src/internal_modules/roc_audio/pcm_mapper_reader.h b/src/internal_modules/roc_audio/pcm_mapper_reader.h
index 6fdfecd07..2f36eed69 100644
--- a/src/internal_modules/roc_audio/pcm_mapper_reader.h
+++ b/src/internal_modules/roc_audio/pcm_mapper_reader.h
@@ -26,7 +26,7 @@ namespace audio {
 //! PCM mapper reader.
 //! Reads frames from nested reader and maps them to another PCM format.
 //! @remarks
-//!  - Either input or output format must be raw samples (Sample_RawFormat).
+//!  - Either input or output format must be raw samples (PcmSubformat_Raw).
 //!  - Both input and output formats must be byte-aligned.
 class PcmMapperReader : public IFrameReader, public core::NonCopyable<> {
 public:
diff --git a/src/internal_modules/roc_audio/pcm_mapper_writer.cpp b/src/internal_modules/roc_audio/pcm_mapper_writer.cpp
index a9ee25584..773d8941c 100644
--- a/src/internal_modules/roc_audio/pcm_mapper_writer.cpp
+++ b/src/internal_modules/roc_audio/pcm_mapper_writer.cpp
@@ -7,7 +7,7 @@
  */
 
 #include "roc_audio/pcm_mapper_writer.h"
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
@@ -25,10 +25,9 @@ PcmMapperWriter::PcmMapperWriter(IFrameWriter& frame_writer,
     , out_spec_(out_spec)
     , num_ch_(out_spec.num_channels())
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid()
-        || in_spec_.sample_format() != SampleFormat_Pcm
-        || out_spec_.sample_format() != SampleFormat_Pcm) {
-        roc_panic("pcm mapper writer: required valid sample specs with pcm format:"
+    if (!in_spec_.is_complete() || !out_spec_.is_complete()
+        || in_spec_.format() != Format_Pcm || out_spec_.format() != Format_Pcm) {
+        roc_panic("pcm mapper writer: required complete sample specs with pcm format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
@@ -49,7 +48,8 @@ PcmMapperWriter::PcmMapperWriter(IFrameWriter& frame_writer,
             sample_spec_to_str(in_spec_).c_str(), sample_spec_to_str(out_spec_).c_str());
     }
 
-    mapper_.reset(new (mapper_) PcmMapper(in_spec_.pcm_format(), out_spec_.pcm_format()));
+    mapper_.reset(new (mapper_)
+                      PcmMapper(in_spec_.pcm_subformat(), out_spec_.pcm_subformat()));
 
     if (mapper_->input_bit_count(1) % 8 != 0 || mapper_->output_bit_count(1) % 8 != 0) {
         roc_panic("pcm mapper writer: unsupported not byte-aligned encoding:"
diff --git a/src/internal_modules/roc_audio/pcm_mapper_writer.h b/src/internal_modules/roc_audio/pcm_mapper_writer.h
index 661435066..0323a276f 100644
--- a/src/internal_modules/roc_audio/pcm_mapper_writer.h
+++ b/src/internal_modules/roc_audio/pcm_mapper_writer.h
@@ -26,7 +26,7 @@ namespace audio {
 //! PCM mapper writer.
 //! Maps frames to another PCM format and writes them to nested writer.
 //! @remarks
-//!  - Either input or output format must be raw samples (Sample_RawFormat).
+//!  - Either input or output format must be raw samples (PcmSubformat_Raw).
 //!  - Both input and output formats must be byte-aligned.
 class PcmMapperWriter : public IFrameWriter, public core::NonCopyable<> {
 public:
diff --git a/src/internal_modules/roc_audio/pcm_format.h b/src/internal_modules/roc_audio/pcm_subformat.h
similarity index 71%
rename from src/internal_modules/roc_audio/pcm_format.h
rename to src/internal_modules/roc_audio/pcm_subformat.h
index 2ad620d3c..c4fec2aae 100644
--- a/src/internal_modules/roc_audio/pcm_format.h
+++ b/src/internal_modules/roc_audio/pcm_subformat.h
@@ -6,199 +6,203 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
-//! @file roc_audio/pcm_format.h
-//! @brief PCM format.
+//! @file roc_audio/pcm_subformat.h
+//! @brief PCM sub-format.
 
-#ifndef ROC_AUDIO_PCM_FORMAT_H_
-#define ROC_AUDIO_PCM_FORMAT_H_
+#ifndef ROC_AUDIO_PCM_SUBFORMAT_H_
+#define ROC_AUDIO_PCM_SUBFORMAT_H_
 
 #include "roc_core/stddefs.h"
 
 namespace roc {
 namespace audio {
 
-//! PCM format.
+//! PCM sub-format.
+//!
 //! Defines PCM sample coding and endian.
+//! Used when Format is set to Format_Pcm or other PCM-based format like
+//! FLAC, ALAC, WAV, etc.
+//!
 //! "Default endian" means use whatever endian is natural in current context, e.g.:
 //!  - for network packets, big endian (a.k.a. network byte order) is default
 //!  - for processing, native CPU endian is default
 //!  - for WAV files, little endian is default
-enum PcmFormat {
+enum PcmSubformat {
     //! Invalid format.
-    PcmFormat_Invalid,
+    PcmSubformat_Invalid,
 
     //! 8-bit signed integer, default endian.
-    PcmFormat_SInt8,
+    PcmSubformat_SInt8,
     //! 8-bit signed integer, big endian.
-    PcmFormat_SInt8_Be,
+    PcmSubformat_SInt8_Be,
     //! 8-bit signed integer, little endian.
-    PcmFormat_SInt8_Le,
+    PcmSubformat_SInt8_Le,
     //! 8-bit unsigned integer, default endian.
-    PcmFormat_UInt8,
+    PcmSubformat_UInt8,
     //! 8-bit unsigned integer, big endian.
-    PcmFormat_UInt8_Be,
+    PcmSubformat_UInt8_Be,
     //! 8-bit unsigned integer, little endian.
-    PcmFormat_UInt8_Le,
+    PcmSubformat_UInt8_Le,
 
     //! 16-bit signed integer, default endian.
-    PcmFormat_SInt16,
+    PcmSubformat_SInt16,
     //! 16-bit signed integer, big endian.
-    PcmFormat_SInt16_Be,
+    PcmSubformat_SInt16_Be,
     //! 16-bit signed integer, little endian.
-    PcmFormat_SInt16_Le,
+    PcmSubformat_SInt16_Le,
     //! 16-bit unsigned integer, default endian.
-    PcmFormat_UInt16,
+    PcmSubformat_UInt16,
     //! 16-bit unsigned integer, big endian.
-    PcmFormat_UInt16_Be,
+    PcmSubformat_UInt16_Be,
     //! 16-bit unsigned integer, little endian.
-    PcmFormat_UInt16_Le,
+    PcmSubformat_UInt16_Le,
 
     //! 18-bit signed integer (2.25 bytes), default endian.
-    PcmFormat_SInt18,
+    PcmSubformat_SInt18,
     //! 18-bit signed integer (2.25 bytes), big endian.
-    PcmFormat_SInt18_Be,
+    PcmSubformat_SInt18_Be,
     //! 18-bit signed integer (2.25 bytes), little endian.
-    PcmFormat_SInt18_Le,
+    PcmSubformat_SInt18_Le,
     //! 18-bit unsigned integer (2.25 bytes), default endian.
-    PcmFormat_UInt18,
+    PcmSubformat_UInt18,
     //! 18-bit unsigned integer (2.25 bytes), big endian.
-    PcmFormat_UInt18_Be,
+    PcmSubformat_UInt18_Be,
     //! 18-bit unsigned integer (2.25 bytes), little endian.
-    PcmFormat_UInt18_Le,
+    PcmSubformat_UInt18_Le,
 
     //! 18-bit signed integer, in low bits of 3-byte container, default endian.
-    PcmFormat_SInt18_3,
+    PcmSubformat_SInt18_3,
     //! 18-bit signed integer, in low bits of 3-byte container, big endian.
-    PcmFormat_SInt18_3_Be,
+    PcmSubformat_SInt18_3_Be,
     //! 18-bit signed integer, in low bits of 3-byte container, little endian.
-    PcmFormat_SInt18_3_Le,
+    PcmSubformat_SInt18_3_Le,
     //! 18-bit unsigned integer, in low bits of 3-byte container, default endian.
-    PcmFormat_UInt18_3,
+    PcmSubformat_UInt18_3,
     //! 18-bit unsigned integer, in low bits of 3-byte container, big endian.
-    PcmFormat_UInt18_3_Be,
+    PcmSubformat_UInt18_3_Be,
     //! 18-bit unsigned integer, in low bits of 3-byte container, little endian.
-    PcmFormat_UInt18_3_Le,
+    PcmSubformat_UInt18_3_Le,
 
     //! 18-bit signed integer, in low bits of 4-byte container, default endian.
-    PcmFormat_SInt18_4,
+    PcmSubformat_SInt18_4,
     //! 18-bit signed integer, in low bits of 4-byte container, big endian.
-    PcmFormat_SInt18_4_Be,
+    PcmSubformat_SInt18_4_Be,
     //! 18-bit signed integer, in low bits of 4-byte container, little endian.
-    PcmFormat_SInt18_4_Le,
+    PcmSubformat_SInt18_4_Le,
     //! 18-bit unsigned integer, in low bits of 4-byte container, default endian.
-    PcmFormat_UInt18_4,
+    PcmSubformat_UInt18_4,
     //! 18-bit unsigned integer, in low bits of 4-byte container, big endian.
-    PcmFormat_UInt18_4_Be,
+    PcmSubformat_UInt18_4_Be,
     //! 18-bit unsigned integer, in low bits of 4-byte container, little endian.
-    PcmFormat_UInt18_4_Le,
+    PcmSubformat_UInt18_4_Le,
 
     //! 20-bit signed integer (2.5 bytes), default endian.
-    PcmFormat_SInt20,
+    PcmSubformat_SInt20,
     //! 20-bit signed integer (2.5 bytes), big endian.
-    PcmFormat_SInt20_Be,
+    PcmSubformat_SInt20_Be,
     //! 20-bit signed integer (2.5 bytes), little endian.
-    PcmFormat_SInt20_Le,
+    PcmSubformat_SInt20_Le,
     //! 20-bit unsigned integer (2.5 bytes), default endian.
-    PcmFormat_UInt20,
+    PcmSubformat_UInt20,
     //! 20-bit unsigned integer (2.5 bytes), big endian.
-    PcmFormat_UInt20_Be,
+    PcmSubformat_UInt20_Be,
     //! 20-bit unsigned integer (2.5 bytes), little endian.
-    PcmFormat_UInt20_Le,
+    PcmSubformat_UInt20_Le,
 
     //! 20-bit signed integer, in low bits of 3-byte container, default endian.
-    PcmFormat_SInt20_3,
+    PcmSubformat_SInt20_3,
     //! 20-bit signed integer, in low bits of 3-byte container, big endian.
-    PcmFormat_SInt20_3_Be,
+    PcmSubformat_SInt20_3_Be,
     //! 20-bit signed integer, in low bits of 3-byte container, little endian.
-    PcmFormat_SInt20_3_Le,
+    PcmSubformat_SInt20_3_Le,
     //! 20-bit unsigned integer, in low bits of 3-byte container, default endian.
-    PcmFormat_UInt20_3,
+    PcmSubformat_UInt20_3,
     //! 20-bit unsigned integer, in low bits of 3-byte container, big endian.
-    PcmFormat_UInt20_3_Be,
+    PcmSubformat_UInt20_3_Be,
     //! 20-bit unsigned integer, in low bits of 3-byte container, little endian.
-    PcmFormat_UInt20_3_Le,
+    PcmSubformat_UInt20_3_Le,
 
     //! 20-bit signed integer, in low bits of 4-byte container, default endian.
-    PcmFormat_SInt20_4,
+    PcmSubformat_SInt20_4,
     //! 20-bit signed integer, in low bits of 4-byte container, big endian.
-    PcmFormat_SInt20_4_Be,
+    PcmSubformat_SInt20_4_Be,
     //! 20-bit signed integer, in low bits of 4-byte container, little endian.
-    PcmFormat_SInt20_4_Le,
+    PcmSubformat_SInt20_4_Le,
     //! 20-bit unsigned integer, in low bits of 4-byte container, default endian.
-    PcmFormat_UInt20_4,
+    PcmSubformat_UInt20_4,
     //! 20-bit unsigned integer, in low bits of 4-byte container, big endian.
-    PcmFormat_UInt20_4_Be,
+    PcmSubformat_UInt20_4_Be,
     //! 20-bit unsigned integer, in low bits of 4-byte container, little endian.
-    PcmFormat_UInt20_4_Le,
+    PcmSubformat_UInt20_4_Le,
 
     //! 24-bit signed integer (3 bytes), default endian.
-    PcmFormat_SInt24,
+    PcmSubformat_SInt24,
     //! 24-bit signed integer (3 bytes), big endian.
-    PcmFormat_SInt24_Be,
+    PcmSubformat_SInt24_Be,
     //! 24-bit signed integer (3 bytes), little endian.
-    PcmFormat_SInt24_Le,
+    PcmSubformat_SInt24_Le,
     //! 24-bit unsigned integer (3 bytes), default endian.
-    PcmFormat_UInt24,
+    PcmSubformat_UInt24,
     //! 24-bit unsigned integer (3 bytes), big endian.
-    PcmFormat_UInt24_Be,
+    PcmSubformat_UInt24_Be,
     //! 24-bit unsigned integer (3 bytes), little endian.
-    PcmFormat_UInt24_Le,
+    PcmSubformat_UInt24_Le,
 
     //! 24-bit signed integer, in low bits of 4-byte container, default endian.
-    PcmFormat_SInt24_4,
+    PcmSubformat_SInt24_4,
     //! 24-bit signed integer, in low bits of 4-byte container, big endian.
-    PcmFormat_SInt24_4_Be,
+    PcmSubformat_SInt24_4_Be,
     //! 24-bit signed integer, in low bits of 4-byte container, little endian.
-    PcmFormat_SInt24_4_Le,
+    PcmSubformat_SInt24_4_Le,
     //! 24-bit unsigned integer, in low bits of 4-byte container, default endian.
-    PcmFormat_UInt24_4,
+    PcmSubformat_UInt24_4,
     //! 24-bit unsigned integer, in low bits of 4-byte container, big endian.
-    PcmFormat_UInt24_4_Be,
+    PcmSubformat_UInt24_4_Be,
     //! 24-bit unsigned integer, in low bits of 4-byte container, little endian.
-    PcmFormat_UInt24_4_Le,
+    PcmSubformat_UInt24_4_Le,
 
     //! 32-bit signed integer, default endian.
-    PcmFormat_SInt32,
+    PcmSubformat_SInt32,
     //! 32-bit signed integer, big endian.
-    PcmFormat_SInt32_Be,
+    PcmSubformat_SInt32_Be,
     //! 32-bit signed integer, little endian.
-    PcmFormat_SInt32_Le,
+    PcmSubformat_SInt32_Le,
     //! 32-bit unsigned integer, default endian.
-    PcmFormat_UInt32,
+    PcmSubformat_UInt32,
     //! 32-bit unsigned integer, big endian.
-    PcmFormat_UInt32_Be,
+    PcmSubformat_UInt32_Be,
     //! 32-bit unsigned integer, little endian.
-    PcmFormat_UInt32_Le,
+    PcmSubformat_UInt32_Le,
 
     //! 64-bit signed integer, default endian.
-    PcmFormat_SInt64,
+    PcmSubformat_SInt64,
     //! 64-bit signed integer, big endian.
-    PcmFormat_SInt64_Be,
+    PcmSubformat_SInt64_Be,
     //! 64-bit signed integer, little endian.
-    PcmFormat_SInt64_Le,
+    PcmSubformat_SInt64_Le,
     //! 64-bit unsigned integer, default endian.
-    PcmFormat_UInt64,
+    PcmSubformat_UInt64,
     //! 64-bit unsigned integer, big endian.
-    PcmFormat_UInt64_Be,
+    PcmSubformat_UInt64_Be,
     //! 64-bit unsigned integer, little endian.
-    PcmFormat_UInt64_Le,
+    PcmSubformat_UInt64_Le,
 
     //! 32-bit IEEE-754 float in range [-1.0; +1.0], default endian.
-    PcmFormat_Float32,
+    PcmSubformat_Float32,
     //! 32-bit IEEE-754 float in range [-1.0; +1.0], big endian.
-    PcmFormat_Float32_Be,
+    PcmSubformat_Float32_Be,
     //! 32-bit IEEE-754 float in range [-1.0; +1.0], little endian.
-    PcmFormat_Float32_Le,
+    PcmSubformat_Float32_Le,
 
     //! 64-bit IEEE-754 float in range [-1.0; +1.0], default endian.
-    PcmFormat_Float64,
+    PcmSubformat_Float64,
     //! 64-bit IEEE-754 float in range [-1.0; +1.0], big endian.
-    PcmFormat_Float64_Be,
+    PcmSubformat_Float64_Be,
     //! 64-bit IEEE-754 float in range [-1.0; +1.0], little endian.
-    PcmFormat_Float64_Le,
+    PcmSubformat_Float64_Le,
 
     //! Maximum enum value.
-    PcmFormat_Max
+    PcmSubformat_Max
 };
 
 //! PCM format flags.
@@ -233,41 +237,49 @@ enum PcmFlags {
 
 //! PCM format meta-information.
 struct PcmTraits {
+    //! Numeric identifier.
+    PcmSubformat id;
+
+    //! String name.
+    const char* name;
+
+    //! Flags.
+    unsigned flags;
+
     //! Number of stored bits per sample in binary form.
     size_t bit_width;
 
     //! Number of significant bits per sample.
     size_t bit_depth;
 
-    //! Flags.
-    unsigned flags;
-
     //! Same format, but with explicit _Be or _Le suffix.
     //! If format is default-endian, suffix is added based on current CPU,
     //! otherwise this field is is same as original format.
-    PcmFormat portable_alias;
+    PcmSubformat portable_alias;
 
     //! Same format but with removed _Be or _Le suffix.
     //! May be equal to original format.
-    PcmFormat default_variant;
+    PcmSubformat default_variant;
 
     //! Same format but with _Le suffix.
     //! May be equal to original format.
-    PcmFormat be_variant;
+    PcmSubformat be_variant;
 
     //! Same format but with _Be suffix.
     //! May be equal to original format.
-    PcmFormat le_variant;
+    PcmSubformat le_variant;
 
     //! Initialize invalid traits.
     PcmTraits()
-        : bit_width(0)
-        , bit_depth(0)
+        : id(PcmSubformat_Invalid)
+        , name(NULL)
         , flags(0)
-        , portable_alias(PcmFormat_Invalid)
-        , default_variant(PcmFormat_Invalid)
-        , be_variant(PcmFormat_Invalid)
-        , le_variant(PcmFormat_Invalid) {
+        , bit_width(0)
+        , bit_depth(0)
+        , portable_alias(PcmSubformat_Invalid)
+        , default_variant(PcmSubformat_Invalid)
+        , be_variant(PcmSubformat_Invalid)
+        , le_variant(PcmSubformat_Invalid) {
     }
 
     //! Check if all given flags are set.
@@ -284,18 +296,18 @@ typedef void (*PcmMapFn)(const uint8_t* in_data,
                          size_t n_samples);
 
 //! Get mapping function for given PCM format pair.
-PcmMapFn pcm_format_mapfn(PcmFormat in_format, PcmFormat out_format);
+PcmMapFn pcm_subformat_mapfn(PcmSubformat in_format, PcmSubformat out_format);
 
 //! Get format traits for given PCM format.
-PcmTraits pcm_format_traits(PcmFormat format);
+PcmTraits pcm_subformat_traits(PcmSubformat format);
 
 //! Get string name of PCM format.
-const char* pcm_format_to_str(PcmFormat format);
+const char* pcm_subformat_to_str(PcmSubformat format);
 
 //! Get PCM format from string name.
-PcmFormat pcm_format_from_str(const char* str);
+PcmSubformat pcm_subformat_from_str(const char* str);
 
 } // namespace audio
 } // namespace roc
 
-#endif // ROC_AUDIO_PCM_FORMAT_H_
+#endif // ROC_AUDIO_PCM_SUBFORMAT_H_
diff --git a/src/internal_modules/roc_audio/pcm_format_gen.py b/src/internal_modules/roc_audio/pcm_subformat_gen.py
similarity index 92%
rename from src/internal_modules/roc_audio/pcm_format_gen.py
rename to src/internal_modules/roc_audio/pcm_subformat_gen.py
index adddd7680..12f0cc30d 100755
--- a/src/internal_modules/roc_audio/pcm_format_gen.py
+++ b/src/internal_modules/roc_audio/pcm_subformat_gen.py
@@ -89,9 +89,9 @@ def compute_octets(code):
 
 # generate enum name for pcm code + endian
 # e.g.:
-#  SInt18_3, Default => PcmFormat_SInt18_3
-#  SInt18_3, Little => PcmFormat_SInt18_3_Le
-#  SInt18_3, Big => PcmFormat_SInt18_3_Be
+#  SInt18_3, Default => PcmSubformat_SInt18_3
+#  SInt18_3, Little => PcmSubformat_SInt18_3_Le
+#  SInt18_3, Big => PcmSubformat_SInt18_3_Be
 def make_enum_name(code, endian):
     name = code['code']
 
@@ -101,13 +101,13 @@ def make_enum_name(code, endian):
         if endian == 'Big':
             name += '_Be'
 
-    return 'PcmFormat_' + name
+    return 'PcmSubformat_' + name
 
 # generate short string name for pcm code + endian
 # e.g.:
-#  SInt18_3, Default => s18_3
-#  SInt18_3, Little => s18_3le
-#  SInt18_3, Big => s18_3be
+#  SInt18_3, Default => pcm_s18_3
+#  SInt18_3, Little => pcm_s18_3le
+#  SInt18_3, Big => pcm_s18_3be
 def make_short_name(code, endian):
     name = code['short_name']
 
@@ -585,10 +585,11 @@ def nth_chars(codes, prefix=()):
 
 template = env.from_string('''
 /*
- * THIS FILE IS AUTO-GENERATED USING `pcm_format_gen.py'. DO NOT EDIT!
+ * THIS FILE IS AUTO-GENERATED USING `pcm_subformat_gen.py'. DO NOT EDIT!
  */
 
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
+#include "roc_audio/pcm_subformat_rw.h"
 #include "roc_core/attributes.h"
 #include "roc_core/cpu_traits.h"
 #include "roc_core/stddefs.h"
@@ -709,7 +710,6 @@ def nth_chars(codes, prefix=()):
 {% endif %}
 {% endfor %}
 {% endfor %}
-
 // N-byte native-endian sample
 template <class T> struct pcm_sample;
 
@@ -733,54 +733,6 @@ def nth_chars(codes, prefix=()):
 };
 
 {% endfor %}
-// Write octet at given byte-aligned bit offset
-inline void pcm_aligned_write(uint8_t* buffer, size_t& bit_offset, uint8_t arg) {
-    buffer[bit_offset >> 3] = arg;
-    bit_offset += 8;
-}
-
-// Read octet at given byte-aligned bit offset
-inline uint8_t pcm_aligned_read(const uint8_t* buffer, size_t& bit_offset) {
-    uint8_t ret = buffer[bit_offset >> 3];
-    bit_offset += 8;
-    return ret;
-}
-
-// Write value (at most 8 bits) at given unaligned bit offset
-inline void
-pcm_unaligned_write(uint8_t* buffer, size_t& bit_offset, size_t bit_length, uint8_t arg) {
-    size_t byte_index = (bit_offset >> 3);
-    size_t bit_index = (bit_offset & 0x7u);
-
-    if (bit_index == 0) {
-        buffer[byte_index] = 0;
-    }
-
-    buffer[byte_index] |= uint8_t(uint8_t(arg << (8 - bit_length)) >> bit_index);
-
-    if (bit_index + bit_length > 8) {
-        buffer[byte_index + 1] = uint8_t(arg << bit_index);
-    }
-
-    bit_offset += bit_length;
-}
-
-// Read value (at most 8 bits) at given unaligned bit offset
-inline uint8_t
-pcm_unaligned_read(const uint8_t* buffer, size_t& bit_offset, size_t bit_length) {
-    size_t byte_index = (bit_offset >> 3);
-    size_t bit_index = (bit_offset & 0x7u);
-
-    uint8_t ret = uint8_t(uint8_t(buffer[byte_index] << bit_index) >> (8 - bit_length));
-
-    if (bit_index + bit_length > 8) {
-        ret |= uint8_t(buffer[byte_index + 1] >> ((8 - bit_index) + (8 - bit_length)));
-    }
-
-    bit_offset += bit_length;
-    return ret;
-}
-
 // Sample packer / unpacker
 template <PcmCode, PcmEndian> struct pcm_packer;
 
@@ -870,7 +822,7 @@ def nth_chars(codes, prefix=()):
 
 // Select mapping function
 template <PcmCode InCode, PcmEndian InEndian>
-PcmMapFn pcm_map_to_raw(PcmFormat raw_format) {
+PcmMapFn pcm_map_to_raw(PcmSubformat raw_format) {
     switch (raw_format) {
 {% for ocode in CODES %}
 {% if ocode.is_raw: %}
@@ -892,7 +844,7 @@ def nth_chars(codes, prefix=()):
 
 // Select mapping function
 template <PcmCode OutCode, PcmEndian OutEndian>
-PcmMapFn pcm_map_from_raw(PcmFormat raw_format) {
+PcmMapFn pcm_map_from_raw(PcmSubformat raw_format) {
     switch (raw_format) {
 {% for icode in CODES %}
 {% if icode.is_raw: %}
@@ -915,7 +867,7 @@ def nth_chars(codes, prefix=()):
 } // namespace
 
 // Select mapping function
-PcmMapFn pcm_format_mapfn(PcmFormat in_format, PcmFormat out_format) {
+PcmMapFn pcm_subformat_mapfn(PcmSubformat in_format, PcmSubformat out_format) {
     // non-raw to raw
     switch (in_format) {
 {% for icode in CODES %}
@@ -983,13 +935,15 @@ def nth_chars(codes, prefix=()):
 }
 
 // Get format traits
-PcmTraits pcm_format_traits(PcmFormat format) {
+PcmTraits pcm_subformat_traits(PcmSubformat format) {
     PcmTraits traits;
 
     switch (format) {
 {% for code in CODES %}
 {% for endian in ENDIANS %}
     case {{ make_enum_name(code, endian) }}:
+        traits.id = {{ make_enum_name(code, endian) }};
+        traits.name = "{{ make_short_name(code, endian) }}";
         traits.bit_width = {{ code.packed_width }};
         traits.bit_depth = {{ code.depth }};
         traits.flags = {{ make_format_flags(code) }};
@@ -1030,7 +984,7 @@ def nth_chars(codes, prefix=()):
     return traits;
 }
 
-const char* pcm_format_to_str(PcmFormat format) {
+const char* pcm_subformat_to_str(PcmSubformat format) {
     switch (format) {
 {% for code in CODES %}
 {% for endian in ENDIANS %}
@@ -1044,9 +998,9 @@ def nth_chars(codes, prefix=()):
     return NULL;
 }
 
-PcmFormat pcm_format_from_str(const char* str) {
+PcmSubformat pcm_subformat_from_str(const char* str) {
     if (!str) {
-        return PcmFormat_Invalid;
+        return PcmSubformat_Invalid;
     }
 {% for c0 in nth_chars(CODES) %}
     if (str[0] == '{{ c0 }}') {
@@ -1063,7 +1017,7 @@ def nth_chars(codes, prefix=()):
 {% endfor %}
 {% endif %}
 {% endfor %}
-                return PcmFormat_Invalid;
+                return PcmSubformat_Invalid;
             }
 {% endfor %}
 {% for code in CODES %}
@@ -1075,13 +1029,13 @@ def nth_chars(codes, prefix=()):
 {% endfor %}
 {% endif %}
 {% endfor %}
-            return PcmFormat_Invalid;
+            return PcmSubformat_Invalid;
         }
 {% endfor %}
-        return PcmFormat_Invalid;
+        return PcmSubformat_Invalid;
     }
 {% endfor %}
-    return PcmFormat_Invalid;
+    return PcmSubformat_Invalid;
 }
 
 } // namespace audio
@@ -1094,5 +1048,5 @@ def nth_chars(codes, prefix=()):
 
 os.chdir(os.path.dirname(os.path.abspath(__file__)))
 
-with open('pcm_format.cpp', 'w') as fp:
+with open('pcm_subformat.cpp', 'w') as fp:
     print(text, file=fp)
diff --git a/src/internal_modules/roc_audio/pcm_subformat_rw.h b/src/internal_modules/roc_audio/pcm_subformat_rw.h
new file mode 100644
index 000000000..023e88131
--- /dev/null
+++ b/src/internal_modules/roc_audio/pcm_subformat_rw.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2022 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+//! @file roc_audio/pcm_subformat_rw.h
+//! @brief PCM sub-format read/write helpers.
+
+#ifndef ROC_AUDIO_PCM_SUBFORMAT_RW_H_
+#define ROC_AUDIO_PCM_SUBFORMAT_RW_H_
+
+#include "roc_audio/pcm_subformat.h"
+#include "roc_core/stddefs.h"
+
+namespace roc {
+namespace audio {
+
+//! Write octet at given byte-aligned bit offset.
+inline void pcm_aligned_write(uint8_t* buffer, size_t& bit_offset, uint8_t arg) {
+    buffer[bit_offset >> 3] = arg;
+    bit_offset += 8;
+}
+
+//! Read octet at given byte-aligned bit offset.
+inline uint8_t pcm_aligned_read(const uint8_t* buffer, size_t& bit_offset) {
+    const uint8_t ret = buffer[bit_offset >> 3];
+    bit_offset += 8;
+    return ret;
+}
+
+//! Write value (at most 8 bits) at given unaligned bit offset.
+inline void
+pcm_unaligned_write(uint8_t* buffer, size_t& bit_offset, size_t bit_length, uint8_t arg) {
+    const size_t byte_index = (bit_offset >> 3);
+    const size_t bit_index = (bit_offset & 0x7u);
+
+    if (bit_index == 0) {
+        buffer[byte_index] = 0;
+    }
+
+    buffer[byte_index] |= uint8_t(uint8_t(arg << (8 - bit_length)) >> bit_index);
+
+    if (bit_index + bit_length > 8) {
+        buffer[byte_index + 1] = uint8_t(arg << bit_index);
+    }
+
+    bit_offset += bit_length;
+}
+
+//! Read value (at most 8 bits) at given unaligned bit offset.
+inline uint8_t
+pcm_unaligned_read(const uint8_t* buffer, size_t& bit_offset, size_t bit_length) {
+    const size_t byte_index = (bit_offset >> 3);
+    const size_t bit_index = (bit_offset & 0x7u);
+
+    uint8_t ret = uint8_t(uint8_t(buffer[byte_index] << bit_index) >> (8 - bit_length));
+
+    if (bit_index + bit_length > 8) {
+        ret |= uint8_t(buffer[byte_index + 1] >> ((8 - bit_index) + (8 - bit_length)));
+    }
+
+    bit_offset += bit_length;
+    return ret;
+}
+
+} // namespace audio
+} // namespace roc
+
+#endif // ROC_AUDIO_PCM_SUBFORMAT_RW_H_
diff --git a/src/internal_modules/roc_audio/plc_reader.cpp b/src/internal_modules/roc_audio/plc_reader.cpp
index d1d9336d0..0544aaee9 100644
--- a/src/internal_modules/roc_audio/plc_reader.cpp
+++ b/src/internal_modules/roc_audio/plc_reader.cpp
@@ -7,7 +7,7 @@
  */
 
 #include "roc_audio/plc_reader.h"
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
@@ -33,8 +33,8 @@ PlcReader::PlcReader(IFrameReader& frame_reader,
     , got_first_signal_(false)
     , sample_spec_(sample_spec)
     , init_status_(status::NoStatus) {
-    if (!sample_spec_.is_valid() || sample_spec_.sample_format() != SampleFormat_Pcm) {
-        roc_panic("plc reader: required valid sample spec with pcm format: spec=%s",
+    if (!sample_spec_.is_complete() || !sample_spec_.is_pcm()) {
+        roc_panic("plc reader: required complete sample spec with pcm format: spec=%s",
                   sample_spec_to_str(sample_spec_).c_str());
     }
     if (sample_spec_ != plc_.sample_spec()) {
diff --git a/src/internal_modules/roc_audio/resampler_reader.cpp b/src/internal_modules/roc_audio/resampler_reader.cpp
index bca675831..34bd0ec5b 100644
--- a/src/internal_modules/roc_audio/resampler_reader.cpp
+++ b/src/internal_modules/roc_audio/resampler_reader.cpp
@@ -27,9 +27,9 @@ ResamplerReader::ResamplerReader(IFrameReader& frame_reader,
     , last_in_cts_(0)
     , scaling_(1.0f)
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid() || !in_spec_.is_raw()
+    if (!in_spec_.is_complete() || !out_spec_.is_complete() || !in_spec_.is_raw()
         || !out_spec_.is_raw()) {
-        roc_panic("resampler reader: required valid sample specs with raw format:"
+        roc_panic("resampler reader: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
diff --git a/src/internal_modules/roc_audio/resampler_writer.cpp b/src/internal_modules/roc_audio/resampler_writer.cpp
index f9945c22b..6a4cedb30 100644
--- a/src/internal_modules/roc_audio/resampler_writer.cpp
+++ b/src/internal_modules/roc_audio/resampler_writer.cpp
@@ -28,9 +28,9 @@ ResamplerWriter::ResamplerWriter(IFrameWriter& frame_writer,
     , out_frame_pos_(0)
     , scaling_(1.f)
     , init_status_(status::NoStatus) {
-    if (!in_spec_.is_valid() || !out_spec_.is_valid() || !in_spec_.is_raw()
+    if (!in_spec_.is_complete() || !out_spec_.is_complete() || !in_spec_.is_raw()
         || !out_spec_.is_raw()) {
-        roc_panic("resampler writer: required valid sample specs with raw format:"
+        roc_panic("resampler writer: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec_).c_str(),
                   sample_spec_to_str(out_spec_).c_str());
diff --git a/src/internal_modules/roc_audio/sample.cpp b/src/internal_modules/roc_audio/sample.cpp
index bec67f725..a31a241d9 100644
--- a/src/internal_modules/roc_audio/sample.cpp
+++ b/src/internal_modules/roc_audio/sample.cpp
@@ -8,7 +8,8 @@
 
 #include "roc_audio/sample.h"
 
-const roc::audio::PcmFormat roc::audio::Sample_RawFormat = roc::audio::PcmFormat_Float32;
+const roc::audio::PcmSubformat roc::audio::PcmSubformat_Raw =
+    roc::audio::PcmSubformat_Float32;
 
 const roc::audio::sample_t roc::audio::Sample_Min = -1;
 const roc::audio::sample_t roc::audio::Sample_Max = 1;
diff --git a/src/internal_modules/roc_audio/sample.h b/src/internal_modules/roc_audio/sample.h
index 14fc41031..2385e098e 100644
--- a/src/internal_modules/roc_audio/sample.h
+++ b/src/internal_modules/roc_audio/sample.h
@@ -12,7 +12,7 @@
 #ifndef ROC_AUDIO_SAMPLE_H_
 #define ROC_AUDIO_SAMPLE_H_
 
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
 #include "roc_core/stddefs.h"
 
 namespace roc {
@@ -22,7 +22,7 @@ namespace audio {
 typedef float sample_t;
 
 //! Format description for raw audio samples.
-extern const PcmFormat Sample_RawFormat;
+extern const PcmSubformat PcmSubformat_Raw;
 
 //! Minimum possible value of a raw sample.
 extern const sample_t Sample_Min;
diff --git a/src/internal_modules/roc_audio/sample_format.cpp b/src/internal_modules/roc_audio/sample_format.cpp
deleted file mode 100644
index 992213b61..000000000
--- a/src/internal_modules/roc_audio/sample_format.cpp
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (c) 2023 Roc Streaming authors
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-#include "roc_audio/sample_format.h"
-
-namespace roc {
-namespace audio {
-
-const char* sample_format_to_str(SampleFormat format) {
-    switch (format) {
-    case SampleFormat_Pcm:
-        return "pcm";
-
-    case SampleFormat_Invalid:
-        break;
-    }
-
-    return "invalid";
-}
-
-} // namespace audio
-} // namespace roc
diff --git a/src/internal_modules/roc_audio/sample_format.h b/src/internal_modules/roc_audio/sample_format.h
deleted file mode 100644
index b449588b0..000000000
--- a/src/internal_modules/roc_audio/sample_format.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (c) 2023 Roc Streaming authors
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-//! @file roc_audio/sample_format.h
-//! @brief Sample format.
-
-#ifndef ROC_AUDIO_SAMPLE_FORMAT_H_
-#define ROC_AUDIO_SAMPLE_FORMAT_H_
-
-#include "roc_audio/pcm_format.h"
-
-namespace roc {
-namespace audio {
-
-//! Sample format.
-//! Defines representation of samples in memory.
-//! Does not define sample rate and channel set.
-enum SampleFormat {
-    //! Invalid format.
-    SampleFormat_Invalid,
-
-    //! Interleaved PCM format.
-    //! What specific PCM coding and endian is used is defined
-    //! by PcmFormat enum.
-    SampleFormat_Pcm,
-};
-
-//! Get string name of sample format.
-const char* sample_format_to_str(SampleFormat format);
-
-} // namespace audio
-} // namespace roc
-
-#endif // ROC_AUDIO_SAMPLE_FORMAT_H_
diff --git a/src/internal_modules/roc_audio/sample_spec.cpp b/src/internal_modules/roc_audio/sample_spec.cpp
index 0dd50c48d..5c9a311fc 100644
--- a/src/internal_modules/roc_audio/sample_spec.cpp
+++ b/src/internal_modules/roc_audio/sample_spec.cpp
@@ -7,8 +7,8 @@
  */
 
 #include "roc_audio/sample_spec.h"
-#include "roc_audio/pcm_format.h"
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/macro_helpers.h"
 #include "roc_core/panic.h"
@@ -62,145 +62,305 @@ core::nanoseconds_t nsamples_2_ns(const float n_samples, const size_t sample_rat
     return (core::nanoseconds_t)val;
 }
 
-PcmFormat get_pcm_portable_format(PcmFormat fmt) {
-    if (fmt == PcmFormat_Invalid) {
-        return PcmFormat_Invalid;
+PcmSubformat get_pcm_portable_format(PcmSubformat fmt) {
+    if (fmt == PcmSubformat_Invalid) {
+        return PcmSubformat_Invalid;
     }
 
-    const PcmTraits traits = pcm_format_traits(fmt);
+    const PcmTraits traits = pcm_subformat_traits(fmt);
     return traits.portable_alias;
 }
 
-size_t get_pcm_sample_width(PcmFormat fmt) {
-    if (fmt == PcmFormat_Invalid) {
+size_t get_pcm_sample_width(PcmSubformat fmt) {
+    if (fmt == PcmSubformat_Invalid) {
         return 0;
     }
 
-    const PcmTraits traits = pcm_format_traits(fmt);
+    const PcmTraits traits = pcm_subformat_traits(fmt);
     return traits.bit_width;
 }
 
 } // namespace
 
 SampleSpec::SampleSpec()
-    : sample_rate_(0)
-    , sample_fmt_(SampleFormat_Invalid)
-    , pcm_fmt_(PcmFormat_Invalid)
-    , pcm_width_(0)
+    : fmt_(Format_Invalid)
+    , has_subfmt_(false)
+    , pcm_subfmt_(PcmSubformat_Invalid)
+    , pcm_subfmt_width_(0)
+    , sample_rate_(0)
     , channel_set_() {
+    fmt_name_[0] = '\0';
+    subfmt_name_[0] = '\0';
 }
 
 SampleSpec::SampleSpec(const size_t sample_rate,
-                       const PcmFormat pcm_fmt,
+                       const PcmSubformat pcm_fmt,
                        const ChannelSet& channel_set)
-    : sample_rate_(0)
-    , sample_fmt_(SampleFormat_Invalid)
-    , pcm_fmt_(PcmFormat_Invalid)
-    , pcm_width_(0)
+    : fmt_(Format_Invalid)
+    , has_subfmt_(false)
+    , pcm_subfmt_(PcmSubformat_Invalid)
+    , pcm_subfmt_width_(0)
+    , sample_rate_(0)
     , channel_set_(channel_set) {
-    set_sample_format(SampleFormat_Pcm);
-    set_pcm_format(pcm_fmt);
+    fmt_name_[0] = '\0';
+    subfmt_name_[0] = '\0';
+
+    set_format(Format_Pcm);
+    set_pcm_subformat(pcm_fmt);
     set_sample_rate(sample_rate);
 
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to construct invalid spec: %s",
+    roc_panic_if_msg(!is_complete(),
+                     "sample spec: attempt to construct incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 }
 
 SampleSpec::SampleSpec(const size_t sample_rate,
-                       const PcmFormat pcm_fmt,
+                       const PcmSubformat pcm_fmt,
                        const ChannelLayout channel_layout,
                        ChannelOrder channel_order,
                        const ChannelMask channel_mask)
-    : sample_rate_(0)
-    , sample_fmt_(SampleFormat_Invalid)
-    , pcm_fmt_(PcmFormat_Invalid)
-    , pcm_width_(0)
+    : fmt_(Format_Invalid)
+    , has_subfmt_(false)
+    , pcm_subfmt_(PcmSubformat_Invalid)
+    , pcm_subfmt_width_(0)
+    , sample_rate_(0)
     , channel_set_(channel_layout, channel_order, channel_mask) {
-    set_sample_format(SampleFormat_Pcm);
-    set_pcm_format(pcm_fmt);
+    fmt_name_[0] = '\0';
+    subfmt_name_[0] = '\0';
+
+    set_format(Format_Pcm);
+    set_pcm_subformat(pcm_fmt);
     set_sample_rate(sample_rate);
 
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to construct invalid spec: %s",
+    roc_panic_if_msg(!is_complete(),
+                     "sample spec: attempt to construct incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 }
 
 bool SampleSpec::operator==(const SampleSpec& other) const {
-    return sample_fmt_ == other.sample_fmt_
-        && (sample_fmt_ != SampleFormat_Pcm
-            || get_pcm_portable_format(pcm_fmt_)
-                == get_pcm_portable_format(other.pcm_fmt_))
-        && sample_rate_ == other.sample_rate_ && channel_set_ == other.channel_set_;
+    // format
+    if (has_format() || other.has_format()) {
+        if (fmt_ != other.fmt_) {
+            return false;
+        }
+        if (fmt_ == Format_Custom && strcmp(fmt_name_, other.fmt_name_) != 0) {
+            return false;
+        }
+    }
+
+    // sub-format
+    if (has_subformat() || other.has_subformat()) {
+        if (pcm_subfmt_ != PcmSubformat_Invalid
+            || other.pcm_subfmt_ != PcmSubformat_Invalid) {
+            if (get_pcm_portable_format(pcm_subfmt_)
+                != get_pcm_portable_format(other.pcm_subfmt_)) {
+                return false;
+            }
+        } else {
+            if (strcmp(subfmt_name_, other.subfmt_name_) != 0) {
+                return false;
+            }
+        }
+    }
+
+    // rate, channels
+    if (sample_rate_ != other.sample_rate_) {
+        return false;
+    }
+    if (channel_set_ != other.channel_set_) {
+        return false;
+    }
+
+    return true;
 }
 
 bool SampleSpec::operator!=(const SampleSpec& other) const {
     return !(*this == other);
 }
 
-bool SampleSpec::is_valid() const {
-    return sample_fmt_ != SampleFormat_Invalid
-        && (sample_fmt_ != SampleFormat_Pcm || pcm_fmt_ != PcmFormat_Invalid)
-        && sample_rate_ != 0 && channel_set_.is_valid();
+bool SampleSpec::is_complete() const {
+    // format
+    if (!has_format()) {
+        return false;
+    }
+    if (!fmt_name_[0]) {
+        return false;
+    }
+
+    // sub-format
+    if (fmt_ == Format_Pcm) {
+        if (pcm_subfmt_ == PcmSubformat_Invalid) {
+            return false;
+        }
+    }
+    if (has_subformat()) {
+        if (!subfmt_name_[0]) {
+            return false;
+        }
+    }
+
+    // rate, channels
+    if (!has_sample_rate()) {
+        return false;
+    }
+    if (!has_channel_set()) {
+        return false;
+    }
+
+    return true;
 }
 
 bool SampleSpec::is_empty() const {
-    return sample_fmt_ == SampleFormat_Invalid && pcm_fmt_ == PcmFormat_Invalid
+    return fmt_ == Format_Invalid && !has_subfmt_ && pcm_subfmt_ == PcmSubformat_Invalid
         && sample_rate_ == 0 && channel_set_.num_channels() == 0;
 }
 
 bool SampleSpec::is_pcm() const {
-    return sample_fmt_ == SampleFormat_Pcm && pcm_fmt_ != PcmFormat_Invalid;
+    return fmt_ == Format_Pcm && pcm_subfmt_ != PcmSubformat_Invalid;
 }
 
 bool SampleSpec::is_raw() const {
-    return sample_fmt_ == SampleFormat_Pcm
-        && get_pcm_portable_format(pcm_fmt_) == get_pcm_portable_format(Sample_RawFormat);
+    return fmt_ == Format_Pcm
+        && get_pcm_portable_format(pcm_subfmt_)
+        == get_pcm_portable_format(PcmSubformat_Raw);
 }
 
 void SampleSpec::clear() {
-    sample_fmt_ = SampleFormat_Invalid;
-    pcm_fmt_ = PcmFormat_Invalid;
-    pcm_width_ = 0;
+    fmt_ = Format_Invalid;
+    fmt_name_[0] = '\0';
+
+    has_subfmt_ = false;
+    subfmt_name_[0] = '\0';
+    pcm_subfmt_ = PcmSubformat_Invalid;
+    pcm_subfmt_width_ = 0;
+
     sample_rate_ = 0;
     channel_set_.clear();
 }
 
-void SampleSpec::use_defaults(PcmFormat default_pcm_fmt,
+void SampleSpec::use_defaults(Format default_fmt,
+                              PcmSubformat default_pcm_fmt,
                               ChannelLayout default_channel_layout,
                               ChannelOrder default_channel_order,
                               ChannelMask default_channel_mask,
                               size_t default_sample_rate) {
-    if (sample_fmt_ == SampleFormat_Invalid && default_pcm_fmt != PcmFormat_Invalid) {
-        set_sample_format(SampleFormat_Pcm);
-        set_pcm_format(default_pcm_fmt);
+    if (!has_format() && default_fmt != Format_Invalid) {
+        set_format(default_fmt);
+    }
+
+    if (!has_subformat() && default_pcm_fmt != PcmSubformat_Invalid) {
+        set_pcm_subformat(default_pcm_fmt);
+    }
+
+    if (!has_sample_rate() && default_sample_rate != 0) {
+        set_sample_rate(default_sample_rate);
     }
-    if (!channel_set_.is_valid() && default_channel_layout != ChanLayout_None) {
+
+    if (!has_channel_set() && default_channel_layout != ChanLayout_None) {
         channel_set_.set_layout(default_channel_layout);
         channel_set_.set_order(default_channel_order);
         channel_set_.set_mask(default_channel_mask);
     }
-    if (sample_rate_ == 0 && default_sample_rate != 0) {
-        set_sample_rate(default_sample_rate);
+}
+
+bool SampleSpec::has_format() const {
+    return fmt_ != Format_Invalid;
+}
+
+Format SampleSpec::format() const {
+    return fmt_;
+}
+
+const char* SampleSpec::format_name() const {
+    return fmt_name_;
+}
+
+void SampleSpec::set_format(Format fmt) {
+    roc_panic_if_msg(fmt < Format_Invalid || fmt >= Format_Max,
+                     "sample spec: invalid format id");
+
+    if (fmt_ == fmt) {
+        return;
+    }
+
+    fmt_ = fmt;
+
+    if (fmt_ == Format_Invalid || fmt_ == Format_Custom) {
+        strcpy(fmt_name_, "");
+    } else {
+        strcpy(fmt_name_, format_to_str(fmt));
     }
 }
 
-SampleFormat SampleSpec::sample_format() const {
-    return sample_fmt_;
+bool SampleSpec::set_custom_format(const char* name) {
+    roc_panic_if_msg(!name, "sample spec: invalid null string");
+
+    const size_t name_len = strlen(name);
+    if (name_len == 0 || name_len >= MaxNameLen) {
+        return false;
+    }
+
+    fmt_ = Format_Custom;
+    strcpy(fmt_name_, name);
+
+    return true;
 }
 
-void SampleSpec::set_sample_format(SampleFormat sample_fmt) {
-    sample_fmt_ = sample_fmt;
+bool SampleSpec::has_subformat() const {
+    return has_subfmt_;
+}
+
+const char* SampleSpec::subformat_name() const {
+    return subfmt_name_;
+}
+
+PcmSubformat SampleSpec::pcm_subformat() const {
+    return pcm_subfmt_;
+}
+
+size_t SampleSpec::pcm_bit_width() const {
+    return pcm_subfmt_width_;
+}
+
+void SampleSpec::set_pcm_subformat(PcmSubformat pcm_fmt) {
+    roc_panic_if_msg(pcm_fmt < PcmSubformat_Invalid || pcm_fmt >= PcmSubformat_Max,
+                     "sample spec: invalid pcm format id");
+
+    if (pcm_subfmt_ == pcm_fmt) {
+        return;
+    }
+
+    pcm_subfmt_ = pcm_fmt;
+    pcm_subfmt_width_ = get_pcm_sample_width(pcm_fmt);
+
+    if (pcm_subfmt_ == PcmSubformat_Invalid) {
+        has_subfmt_ = false;
+        strcpy(subfmt_name_, "");
+    } else {
+        has_subfmt_ = true;
+        strcpy(subfmt_name_, pcm_subformat_to_str(pcm_subfmt_));
+    }
 }
 
-PcmFormat SampleSpec::pcm_format() const {
-    if (sample_fmt_ != SampleFormat_Pcm) {
-        return PcmFormat_Invalid;
+bool SampleSpec::set_custom_subformat(const char* name) {
+    roc_panic_if_msg(!name, "sample spec: string is null");
+
+    const size_t name_len = strlen(name);
+    if (name_len == 0 || name_len >= MaxNameLen) {
+        return false;
     }
-    return pcm_fmt_;
+
+    pcm_subfmt_ = PcmSubformat_Invalid;
+    pcm_subfmt_width_ = 0;
+
+    has_subfmt_ = true;
+    strcpy(subfmt_name_, name);
+
+    return true;
 }
 
-void SampleSpec::set_pcm_format(PcmFormat pcm_fmt) {
-    pcm_fmt_ = pcm_fmt;
-    pcm_width_ = get_pcm_sample_width(pcm_fmt);
+bool SampleSpec::has_sample_rate() const {
+    return sample_rate_ != 0;
 }
 
 size_t SampleSpec::sample_rate() const {
@@ -211,6 +371,14 @@ void SampleSpec::set_sample_rate(const size_t sample_rate) {
     sample_rate_ = sample_rate;
 }
 
+bool SampleSpec::has_channel_set() const {
+    return channel_set_.is_valid();
+}
+
+size_t SampleSpec::num_channels() const {
+    return channel_set_.num_channels();
+}
+
 const ChannelSet& SampleSpec::channel_set() const {
     return channel_set_;
 }
@@ -223,12 +391,8 @@ void SampleSpec::set_channel_set(const ChannelSet& channel_set) {
     channel_set_ = channel_set;
 }
 
-size_t SampleSpec::num_channels() const {
-    return channel_set_.num_channels();
-}
-
 size_t SampleSpec::ns_2_samples_per_chan(const core::nanoseconds_t ns_duration) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     roc_panic_if_msg(ns_duration < 0, "sample spec: duration should not be negative");
@@ -237,21 +401,21 @@ size_t SampleSpec::ns_2_samples_per_chan(const core::nanoseconds_t ns_duration)
 }
 
 core::nanoseconds_t SampleSpec::samples_per_chan_2_ns(const size_t n_samples) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return nsamples_2_ns((float)n_samples, sample_rate_);
 }
 
 core::nanoseconds_t SampleSpec::fract_samples_per_chan_2_ns(const float n_samples) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return nsamples_2_ns(n_samples, sample_rate_);
 }
 
 size_t SampleSpec::ns_2_samples_overall(const core::nanoseconds_t ns_duration) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     roc_panic_if_msg(ns_duration < 0, "sample spec: duration should not be negative");
@@ -260,7 +424,7 @@ size_t SampleSpec::ns_2_samples_overall(const core::nanoseconds_t ns_duration) c
 }
 
 core::nanoseconds_t SampleSpec::samples_overall_2_ns(const size_t n_samples) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     roc_panic_if_msg(n_samples % num_channels() != 0,
@@ -270,7 +434,7 @@ core::nanoseconds_t SampleSpec::samples_overall_2_ns(const size_t n_samples) con
 }
 
 core::nanoseconds_t SampleSpec::fract_samples_overall_2_ns(const float n_samples) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return nsamples_2_ns(n_samples / num_channels(), sample_rate_);
@@ -278,7 +442,7 @@ core::nanoseconds_t SampleSpec::fract_samples_overall_2_ns(const float n_samples
 
 packet::stream_timestamp_t
 SampleSpec::ns_2_stream_timestamp(const core::nanoseconds_t ns_duration) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     roc_panic_if_msg(ns_duration < 0, "sample spec: duration should not be negative");
@@ -288,7 +452,7 @@ SampleSpec::ns_2_stream_timestamp(const core::nanoseconds_t ns_duration) const {
 
 core::nanoseconds_t
 SampleSpec::stream_timestamp_2_ns(const packet::stream_timestamp_t sts_duration) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return nsamples_2_ns((float)sts_duration, sample_rate_);
@@ -300,7 +464,7 @@ double SampleSpec::stream_timestamp_2_ms(packet::stream_timestamp_t sts_duration
 
 packet::stream_timestamp_diff_t
 SampleSpec::ns_2_stream_timestamp_delta(const core::nanoseconds_t ns_delta) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return ns_2_int_samples<packet::stream_timestamp_diff_t>(ns_delta, sample_rate_, 1);
@@ -308,7 +472,7 @@ SampleSpec::ns_2_stream_timestamp_delta(const core::nanoseconds_t ns_delta) cons
 
 core::nanoseconds_t SampleSpec::stream_timestamp_delta_2_ns(
     const packet::stream_timestamp_diff_t sts_delta) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return nsamples_2_ns((float)sts_delta, sample_rate_);
@@ -320,33 +484,31 @@ SampleSpec::stream_timestamp_delta_2_ms(packet::stream_timestamp_diff_t sts_delt
 }
 
 packet::stream_timestamp_t SampleSpec::bytes_2_stream_timestamp(size_t n_bytes) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
-    roc_panic_if_msg(sample_fmt_ != SampleFormat_Pcm,
-                     "sample spec: sample format is not pcm: %s",
+    roc_panic_if_msg(fmt_ != Format_Pcm, "sample spec: sample format is not pcm: %s",
                      sample_spec_to_str(*this).c_str());
 
-    roc_panic_if_msg(pcm_width_ % 8 != 0,
+    roc_panic_if_msg(pcm_subfmt_width_ % 8 != 0,
                      "sample spec: sample width is not byte-aligned: %s",
                      sample_spec_to_str(*this).c_str());
 
-    return n_bytes / (pcm_width_ / 8) / channel_set_.num_channels();
+    return n_bytes / (pcm_subfmt_width_ / 8) / channel_set_.num_channels();
 }
 
 size_t SampleSpec::stream_timestamp_2_bytes(packet::stream_timestamp_t duration) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
-    roc_panic_if_msg(sample_fmt_ != SampleFormat_Pcm,
-                     "sample spec: sample format is not pcm: %s",
+    roc_panic_if_msg(fmt_ != Format_Pcm, "sample spec: sample format is not pcm: %s",
                      sample_spec_to_str(*this).c_str());
 
-    roc_panic_if_msg(pcm_width_ % 8 != 0,
+    roc_panic_if_msg(pcm_subfmt_width_ % 8 != 0,
                      "sample spec: sample width is not byte-aligned: %s",
                      sample_spec_to_str(*this).c_str());
 
-    return duration * (pcm_width_ / 8) * channel_set_.num_channels();
+    return duration * (pcm_subfmt_width_ / 8) * channel_set_.num_channels();
 }
 
 core::nanoseconds_t SampleSpec::bytes_2_ns(size_t n_bytes) const {
@@ -358,7 +520,7 @@ size_t SampleSpec::ns_2_bytes(core::nanoseconds_t duration) const {
 }
 
 void SampleSpec::validate_frame(Frame& frame) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     if (frame.num_bytes() == 0) {
@@ -399,10 +561,10 @@ void SampleSpec::validate_frame(Frame& frame) const {
 }
 
 bool SampleSpec::is_valid_frame_size(size_t n_bytes) {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
-    if (sample_fmt_ != SampleFormat_Pcm || pcm_width_ % 8 != 0) {
+    if (fmt_ != Format_Pcm || pcm_subfmt_width_ % 8 != 0) {
         return true;
     }
 
@@ -415,7 +577,7 @@ bool SampleSpec::is_valid_frame_size(size_t n_bytes) {
     roc_log(LogError,
             "sample spec: invalid frame buffer size: should be multiple of %u, got %lu"
             " (%u bytes per sample, %u channels)",
-            (unsigned)factor, (unsigned long)n_bytes, (unsigned)(pcm_width_ / 8),
+            (unsigned)factor, (unsigned long)n_bytes, (unsigned)(pcm_subfmt_width_ / 8),
             (unsigned)num_channels());
 
     return false;
@@ -424,7 +586,7 @@ bool SampleSpec::is_valid_frame_size(size_t n_bytes) {
 packet::stream_timestamp_t
 SampleSpec::cap_frame_duration(packet::stream_timestamp_t duration,
                                size_t buffer_size) const {
-    roc_panic_if_msg(!is_valid(), "sample spec: attempt to use invalid spec: %s",
+    roc_panic_if_msg(!is_complete(), "sample spec: attempt to use incomplete spec: %s",
                      sample_spec_to_str(*this).c_str());
 
     return std::min(duration, bytes_2_stream_timestamp(buffer_size));
diff --git a/src/internal_modules/roc_audio/sample_spec.h b/src/internal_modules/roc_audio/sample_spec.h
index cd3b48762..405ae3352 100644
--- a/src/internal_modules/roc_audio/sample_spec.h
+++ b/src/internal_modules/roc_audio/sample_spec.h
@@ -13,11 +13,11 @@
 #define ROC_AUDIO_SAMPLE_SPEC_H_
 
 #include "roc_audio/channel_set.h"
+#include "roc_audio/format.h"
 #include "roc_audio/frame.h"
 #include "roc_audio/frame_factory.h"
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_audio/sample.h"
-#include "roc_audio/sample_format.h"
 #include "roc_core/attributes.h"
 #include "roc_core/stddefs.h"
 #include "roc_core/string_builder.h"
@@ -28,7 +28,7 @@ namespace roc {
 namespace audio {
 
 //! Sample specification.
-//! Describes sample rate and channels.
+//! Describes format, rate, and channels.
 class SampleSpec {
 public:
     //! Construct empty specification.
@@ -36,17 +36,17 @@ class SampleSpec {
 
     //! Construct specification with parameters.
     //! @note
-    //!  This constructor sets sample_format() to SampleFormat_Pcm.
-    SampleSpec(size_t sample_rate, PcmFormat pcm_fmt, const ChannelSet& channel_set);
+    //!  This constructor sets format() to Format_Pcm.
+    SampleSpec(size_t sample_rate, PcmSubformat pcm_fmt, const ChannelSet& channel_set);
 
     //! Construct specification with parameters.
     //! @remarks
     //!  This is a convenient overload for the case when 32-bit mask is enough to
     //!  describe channels. Otherwise, use overload that accepts ChannelSet.
     //! @note
-    //!  This constructor sets sample_format() to SampleFormat_Pcm.
+    //!  This constructor sets format() to Format_Pcm.
     SampleSpec(size_t sample_rate,
-               PcmFormat pcm_fmt,
+               PcmSubformat pcm_fmt,
                ChannelLayout channel_layout,
                ChannelOrder channel_order,
                ChannelMask channel_mask);
@@ -65,22 +65,16 @@ class SampleSpec {
     //! @name Getters and setters
     //! @{
 
-    //! Check if sample spec has non-zero rate and valid channel set.
-    bool is_valid() const;
+    //! True if all required fields are set and valid.
+    bool is_complete() const;
 
-    //! Check if sample spec has a zero rate, empty channel set, and invalid_format.
+    //! True if all fields are unset.
     bool is_empty() const;
 
-    //! Check if samples are in PCM format.
-    //! @returns
-    //!  true if sample_format() is SampleFormat_Pcm and pcm_format()
-    //!  is anything except PcmFormat_Invalid.
+    //! True if format is PCM and sub-format is valid PCM encoding.
     bool is_pcm() const;
 
-    //! Check if samples are in raw format.
-    //! @returns
-    //!  true if sample_format() is SampleFormat_Pcm and pcm_format()
-    //!  is Sample_RawFormat (32-bit native-endian floats).
+    //! True if format is PCM and sub-format is PcmSubformat_Raw.
     bool is_raw() const;
 
     //! Unset all fields.
@@ -90,12 +84,75 @@ class SampleSpec {
     //! @remarks
     //!  Updates only those fields which don't have values,
     //!  with corresponding values provided as arguments.
-    void use_defaults(PcmFormat default_pcm_fmt,
+    void use_defaults(Format default_fmt,
+                      PcmSubformat default_pcm_fmt,
                       ChannelLayout default_channel_layout,
                       ChannelOrder default_channel_order,
                       ChannelMask default_channel_mask,
                       size_t default_sample_rate);
 
+    //! True format is set to a valid value.
+    bool has_format() const;
+
+    //! Get format id.
+    //! @remarks
+    //!  Format and sub-format define how samples are represented in memory.
+    //!  What kind of sub-format is used depends on format, e.g. if format()
+    //!  is Format_Pcm(), pcm_subformat() is used.
+    Format format() const;
+
+    //! Get format name.
+    //! @remarks
+    //!  If set_custom_format() was called, returns custom format name. Otherwise,
+    //!  returns string name of format() enum value.
+    const char* format_name() const;
+
+    //! Set format id.
+    void set_format(Format sample_fmt);
+
+    //! Store custom format name and set format to Format_Custom.
+    //! @remarks
+    //!  Custom format and sub-format names are used for file I/O. We can't and don't
+    //!  need to maintain enums for all possible file formats, instead we just forward
+    //!  string format name to the file I/O library.
+    //! @returns
+    //!  false if name is too long.
+    ROC_ATTR_NODISCARD bool set_custom_format(const char* name);
+
+    //! True if sub-format is set.
+    bool has_subformat() const;
+
+    //! Get sub-format name.
+    //! @remarks
+    //!  If set_custom_subformat() was called, returns custom sub-format name.
+    //!  Otherwise, returns string name of sub-format enum value, e.g. if
+    //!  pmc_subformat() is used, returns its string name.
+    const char* subformat_name() const;
+
+    //! Get PCM sub-format.
+    //! @remarks
+    //!  Set only if sub-format is PCM.
+    PcmSubformat pcm_subformat() const;
+
+    //! Get number of bits in PCM sample.
+    //! @remarks
+    //!  Set only if sub-format is PCM.
+    size_t pcm_bit_width() const;
+
+    //! Set PCM sub-format.
+    void set_pcm_subformat(PcmSubformat pcm_fmt);
+
+    //! Store custom sub-format name.
+    //! @remarks
+    //!  Custom format and sub-format names are used for file I/O.
+    //!  See comment for set_custom_format() for rationale.
+    //! @returns
+    //!  false if name is too long.
+    ROC_ATTR_NODISCARD bool set_custom_subformat(const char* name);
+
+    //! True if rate is set to non-zero value.
+    bool has_sample_rate() const;
+
     //! Get sample rate.
     //! @remarks
     //!  Defines sample frequency (number of samples per second).
@@ -104,24 +161,13 @@ class SampleSpec {
     //! Set sample rate.
     void set_sample_rate(size_t sample_rate);
 
-    //! Get sample format.
-    //! @remarks
-    //!  Defines how samples are represented in memory.
-    //!  When set to SampleFormat_Pcm, pcm_format() defines what exact PCM coding
-    //!  and endian are used.
-    SampleFormat sample_format() const;
+    //! True if channel set is valid.
+    bool has_channel_set() const;
 
-    //! Set sample format.
-    void set_sample_format(SampleFormat sample_fmt);
-
-    //! Get PCM format.
+    //! Get number enabled channels in channel set.
     //! @remarks
-    //!  When sample_format() is set to SampleFormat_Pcm, defines what exact PCM coding
-    //!  and endian are used.
-    PcmFormat pcm_format() const;
-
-    //! Set PCM format.
-    void set_pcm_format(PcmFormat pcm_fmt);
+    //!  Shorthand for channel_set().num_channels().
+    size_t num_channels() const;
 
     //! Get channel set.
     //! @remarks
@@ -134,11 +180,6 @@ class SampleSpec {
     //! Set channel set.
     void set_channel_set(const ChannelSet& channel_set);
 
-    //! Get number enabled channels in channel set.
-    //! @remarks
-    //!  Shorthand for channel_set().num_channels().
-    size_t num_channels() const;
-
     // @}
 
     //! @name Convert number of samples
@@ -227,22 +268,22 @@ class SampleSpec {
 
     //! Convert byte size to stream timestamp.
     //! @pre
-    //!  sample_format() should be PCM.
+    //!  format() should be PCM.
     packet::stream_timestamp_t bytes_2_stream_timestamp(size_t n_bytes) const;
 
     //! Convert stream timestamp to byte size.
     //! @pre
-    //!  sample_format() should be PCM.
+    //!  format() should be PCM.
     size_t stream_timestamp_2_bytes(packet::stream_timestamp_t duration) const;
 
     //! Convert byte size to nanosecond duration.
     //! @pre
-    //!  sample_format() should be PCM.
+    //!  format() should be PCM.
     core::nanoseconds_t bytes_2_ns(size_t n_bytes) const;
 
     //! Convert nanosecond duration to byte size.
     //! @pre
-    //!  sample_format() should be PCM.
+    //!  format() should be PCM.
     size_t ns_2_bytes(core::nanoseconds_t duration) const;
 
     // @}
@@ -266,10 +307,17 @@ class SampleSpec {
     // @}
 
 private:
+    enum { MaxNameLen = 8 };
+
+    Format fmt_;
+    char fmt_name_[MaxNameLen];
+
+    bool has_subfmt_;
+    char subfmt_name_[MaxNameLen];
+    PcmSubformat pcm_subfmt_;
+    size_t pcm_subfmt_width_;
+
     size_t sample_rate_;
-    SampleFormat sample_fmt_;
-    PcmFormat pcm_fmt_;
-    size_t pcm_width_;
     ChannelSet channel_set_;
 };
 
diff --git a/src/internal_modules/roc_audio/sample_spec_format.cpp b/src/internal_modules/roc_audio/sample_spec_format.cpp
index 76e018c0c..a9fc66532 100644
--- a/src/internal_modules/roc_audio/sample_spec_format.cpp
+++ b/src/internal_modules/roc_audio/sample_spec_format.cpp
@@ -15,11 +15,14 @@ void format_sample_spec(const SampleSpec& sample_spec, core::StringBuilder& bld)
     bld.append_str("<sspec rate=");
     bld.append_uint(sample_spec.sample_rate(), 10);
     bld.append_str(" fmt=<");
-    bld.append_str(sample_format_to_str(sample_spec.sample_format()));
-    if (sample_spec.sample_format() == SampleFormat_Pcm) {
-        const char* str = pcm_format_to_str(sample_spec.pcm_format());
+    if (sample_spec.has_format()) {
+        bld.append_str(sample_spec.format_name());
+    } else {
+        bld.append_str("none");
+    }
+    if (sample_spec.has_subformat()) {
         bld.append_str(" ");
-        bld.append_str(str ? str : "invalid");
+        bld.append_str(sample_spec.subformat_name());
     }
     bld.append_str(">");
     bld.append_str(" chset=");
diff --git a/src/internal_modules/roc_audio/sample_spec_parse.rl b/src/internal_modules/roc_audio/sample_spec_parse.rl
index a38c93576..91a5c5ee5 100644
--- a/src/internal_modules/roc_audio/sample_spec_parse.rl
+++ b/src/internal_modules/roc_audio/sample_spec_parse.rl
@@ -109,6 +109,50 @@ bool parse_sample_rate(const char* str, size_t str_len, size_t& result) {
     return true;
 }
 
+bool parse_format(const char* str, SampleSpec& sample_spec) {
+    const Format fmt = format_from_str(str);
+
+    if (fmt != Format_Invalid) {
+        sample_spec.set_format(fmt);
+        return true;
+    }
+
+    if (sample_spec.set_custom_format(str)) {
+        return true;
+    }
+
+    return false;
+}
+
+bool parse_subformat(const char* str, SampleSpec& sample_spec) {
+    switch (sample_spec.format()) {
+    case Format_Pcm:
+    case Format_Wav:
+    case Format_Custom: {
+        const PcmSubformat pcm_fmt = pcm_subformat_from_str(str);
+
+        if (pcm_fmt != PcmSubformat_Invalid) {
+            // This is PCM sub-format.
+            sample_spec.set_pcm_subformat(pcm_fmt);
+            return true;
+        }
+
+        if (sample_spec.format() != Format_Pcm) {
+            // This is custom sub-format. Allow only if format is not PCM.
+            if (sample_spec.set_custom_subformat(str)) {
+                return true;
+            }
+        }
+    } break;
+
+    case Format_Invalid:
+    case Format_Max:
+        break;
+    }
+
+    return false;
+}
+
 bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
     if (!str) {
         roc_log(LogError, "parse sample spec: input string is null");
@@ -142,7 +186,8 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
         action set_surround_mask {
             ChannelMask ch_mask = 0;
             if (!parse_surround_mask(start_p, p - start_p, ch_mask)) {
-                roc_log(LogError, "parse sample spec: invalid channel mask name");
+                roc_log(LogError, "parse sample spec: invalid channel mask name '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
                 return false;
             }
             sample_spec.channel_set().set_mask(ch_mask);
@@ -151,7 +196,8 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
         action set_surround_channel {
             ChannelPosition ch_pos = ChanPos_Max;
             if (!parse_surround_channel(start_p, p - start_p, ch_pos)) {
-                roc_log(LogError, "parse sample spec: invalid channel name");
+                roc_log(LogError, "parse sample spec: invalid channel name '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
                 return false;
             }
             sample_spec.channel_set().toggle_channel(ch_pos, true);
@@ -165,8 +211,9 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
         action set_mtr_number {
             size_t ch_pos = 0;
             if (!parse_multitrack_channel(start_p, p - start_p, ch_pos)) {
-                roc_log(LogError, "parse sample spec: invalid channel number,"
+                roc_log(LogError, "parse sample spec: invalid channel number '%.*s' in '%s',"
                     " should be integer in range [0; %d]",
+                    int(p - start_p), start_p, str,
                     (int)ChannelSet::max_channels() - 1);
                 return false;
             }
@@ -175,8 +222,9 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
 
         action set_mtr_range_begin {
             if (!parse_multitrack_channel(start_p, p - start_p, mtr_range_begin)) {
-                roc_log(LogError, "parse sample spec: invalid channel number,"
+                roc_log(LogError, "parse sample spec: invalid channel number '%.*s',"
                     " should be integer in range [0; %d]",
+                    int(p - start_p), start_p,
                     (int)ChannelSet::max_channels() - 1);
                 return false;
             }
@@ -184,8 +232,9 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
 
         action set_mtr_range_end {
             if (!parse_multitrack_channel(start_p, p - start_p, mtr_range_end)) {
-                roc_log(LogError, "parse sample spec: invalid channel number,"
+                roc_log(LogError, "parse sample spec: invalid channel number '%.*s' in '%s',"
                     " should be integer in range [0; %d]",
+                    int(p - start_p), start_p, str,
                     (int)ChannelSet::max_channels() - 1);
                 return false;
             }
@@ -198,7 +247,8 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
 
         action set_mtr_mask {
             if (!parse_multitrack_mask(start_p, p - start_p, sample_spec.channel_set())) {
-                roc_log(LogError, "parse sample spec: invalid channel mask");
+                roc_log(LogError, "parse sample spec: invalid channel mask '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
                 return false;
             }
         }
@@ -209,21 +259,40 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
         }
 
         action set_format {
-            char str[16] = {};
-            strncat(str, start_p, p - start_p);
-            PcmFormat pcm_fmt = pcm_format_from_str(str);
-            if (pcm_fmt == PcmFormat_Invalid) {
-                roc_log(LogError, "parse sample spec: invalid sample format");
+            char buf[16] = {};
+            if (p - start_p > sizeof(buf) - 1) {
+                roc_log(LogError, "parse sample spec: invalid format '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
+                return false;
+            }
+            strncat(buf, start_p, p - start_p);
+            if (!parse_format(buf, sample_spec)) {
+                roc_log(LogError, "parse sample spec: invalid format '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
+                return false;
+            }
+        }
+
+        action set_subformat {
+            char buf[16] = {};
+            if (p - start_p > sizeof(buf) - 1) {
+                roc_log(LogError, "parse sample spec: invalid subformat '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
+                return false;
+            }
+            strncat(buf, start_p, p - start_p);
+            if (!parse_subformat(buf, sample_spec)) {
+                roc_log(LogError, "parse sample spec: invalid subformat '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
                 return false;
             }
-            sample_spec.set_sample_format(SampleFormat_Pcm);
-            sample_spec.set_pcm_format(pcm_fmt);
         }
 
         action set_rate {
             size_t rate = 0;
             if (!parse_sample_rate(start_p, p - start_p, rate)) {
-                roc_log(LogError, "parse sample spec: invalid sample rate");
+                roc_log(LogError, "parse sample spec: invalid sample rate '%.*s' in '%s'",
+                    int(p - start_p), start_p, str);
                 return false;
             }
             sample_spec.set_sample_rate(rate);
@@ -248,11 +317,12 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
         mtr = (mtr_mask | mtr_list) %set_mtr;
 
         format = [a-z0-9_]+ >start_token %set_format;
+        subformat = [a-z0-9_]+ >start_token %set_subformat;
         rate = [0-9]+ >start_token %set_rate;
         channels_DISABLED = surround | mtr;
         channels = ('stereo' | 'mono') >start_token %set_surround_mask %set_surround;
 
-        main := ( ('-' | format) '/' ('-' | rate) '/' ('-' | channels) )
+        main := ( ('-' | format ('@' subformat)?) '/' ('-' | rate) '/' ('-' | channels) )
                 %{ success = true; }
                 ;
 
@@ -262,18 +332,36 @@ bool parse_sample_spec_imp(const char* str, SampleSpec& sample_spec) {
 
     if (!success) {
         roc_log(LogError,
-                "parse sample spec: expected 'FORMAT/RATE/CHANNELS', got '%s'",
-                str);
+                "parse sample spec: expected '<format>[@<subformat>]/<rate>/<channels>',"
+                " got '%s'", str);
         return false;
     }
 
     return true;
 }
 
+bool validate_sample_spec(const SampleSpec& sample_spec) {
+    switch (sample_spec.format()) {
+    case Format_Pcm:
+        if (!sample_spec.has_subformat()) {
+            roc_log(LogError, "parse sample spec: subformat required when format is pcm");
+            return false;
+        }
+        break;
+
+    case Format_Invalid:
+    case Format_Custom:
+    case Format_Max:
+        break;
+    }
+
+    return true;
+}
+
 } // namespace
 
 bool parse_sample_spec(const char* str, SampleSpec& result) {
-    if (!parse_sample_spec_imp(str, result)) {
+    if (!parse_sample_spec_imp(str, result) || !validate_sample_spec(result)) {
         result.clear();
         return false;
     }
diff --git a/src/internal_modules/roc_audio/target_speexdsp/roc_audio/speex_resampler.cpp b/src/internal_modules/roc_audio/target_speexdsp/roc_audio/speex_resampler.cpp
index b62b7e8df..aa35e7c1d 100644
--- a/src/internal_modules/roc_audio/target_speexdsp/roc_audio/speex_resampler.cpp
+++ b/src/internal_modules/roc_audio/target_speexdsp/roc_audio/speex_resampler.cpp
@@ -60,9 +60,9 @@ SpeexResampler::SpeexResampler(const ResamplerConfig& config,
     , in_latency_diff_(0)
     , report_limiter_(LogReportInterval)
     , init_status_(status::NoStatus) {
-    if (!in_spec.is_valid() || !out_spec.is_valid() || !in_spec.is_raw()
+    if (!in_spec.is_complete() || !out_spec.is_complete() || !in_spec.is_raw()
         || !out_spec.is_raw()) {
-        roc_panic("speex resampler: required valid sample specs with raw format:"
+        roc_panic("speex resampler: required complete sample specs with raw format:"
                   " in_spec=%s out_spec=%s",
                   sample_spec_to_str(in_spec).c_str(),
                   sample_spec_to_str(out_spec).c_str());
diff --git a/src/internal_modules/roc_core/backtrace.h b/src/internal_modules/roc_core/backtrace.h
index 7c317106b..d404d9285 100644
--- a/src/internal_modules/roc_core/backtrace.h
+++ b/src/internal_modules/roc_core/backtrace.h
@@ -37,7 +37,7 @@ void print_backtrace_safe();
 //!  @p demangled_buf and @p demangled_size specify the buffer for demangled name.
 //!  When necessary, this function malloc()s or realloc()s @p demangled_buf and
 //!  updates @p demangled_size accordingly. The buffer may be NULL. The buffer may
-//!  be resused across several calls. The user should manually free() the buffer
+//!  be reused across several calls. The user should manually free() the buffer
 //!  when it's not needed anymore.
 //! @returns
 //!  demangled symbol or NULL if the symbol can't be demangled.
diff --git a/src/internal_modules/roc_core/string_list.cpp b/src/internal_modules/roc_core/string_list.cpp
index 29176c50b..e27aebb30 100644
--- a/src/internal_modules/roc_core/string_list.cpp
+++ b/src/internal_modules/roc_core/string_list.cpp
@@ -13,10 +13,37 @@
 namespace roc {
 namespace core {
 
+namespace {
+
+int strcmp_lexical(const char* a, const char* b) {
+    return strcmp(a, b);
+}
+
+int strcmp_natural(const char* a, const char* b) {
+    while (*a && *b) {
+        if (isdigit(*a) && isdigit(*b)) {
+            const long ia = strtol(a, const_cast<char**>((const char**)&a), 10);
+            const long ib = strtol(b, const_cast<char**>((const char**)&b), 10);
+            if (ia != ib) {
+                return ia < ib ? -1 : 1;
+            }
+        } else {
+            if (*a != *b) {
+                return *a < *b ? -1 : 1;
+            }
+            a++;
+            b++;
+        }
+    }
+    return *a < *b ? -1 : *a != *b;
+}
+
+} // namespace
+
 StringList::StringList(IArena& arena)
-    : data_(arena)
-    , front_(NULL)
-    , back_(NULL)
+    : memory_(arena)
+    , head_off_(0)
+    , tail_off_(0)
     , size_(0) {
 }
 
@@ -30,7 +57,7 @@ bool StringList::is_empty() const {
 
 const char* StringList::front() const {
     if (size_) {
-        return front_->str;
+        return from_offset_(head_off_)->str;
     } else {
         return NULL;
     }
@@ -38,7 +65,7 @@ const char* StringList::front() const {
 
 const char* StringList::back() const {
     if (size_) {
-        return back_->str;
+        return from_offset_(tail_off_)->str;
     } else {
         return NULL;
     }
@@ -51,14 +78,12 @@ const char* StringList::nextof(const char* str) const {
 
     check_member_(str);
 
-    const Header* str_header = ROC_CONTAINER_OF(const_cast<char*>(str), Header, str);
-
-    if (str_header == back_) {
+    const Header* curr_header = ROC_CONTAINER_OF(const_cast<char*>(str), Header, str);
+    if (curr_header == from_offset_(tail_off_)) {
         return NULL;
     }
 
-    const Header* next_header =
-        (const Header*)((const char*)str_header + str_header->len);
+    const Header* next_header = from_offset_(curr_header->next_off);
     return next_header->str;
 }
 
@@ -69,23 +94,19 @@ const char* StringList::prevof(const char* str) const {
 
     check_member_(str);
 
-    const Header* str_header = ROC_CONTAINER_OF(const_cast<char*>(str), Header, str);
-
-    if (str_header == front_) {
+    const Header* curr_header = ROC_CONTAINER_OF(const_cast<char*>(str), Header, str);
+    if (curr_header == from_offset_(head_off_)) {
         return NULL;
     }
 
-    const Footer* prev_footer = (const Footer*)((const char*)str_header - sizeof(Footer));
-    const Header* prev_header =
-        (const Header*)((const char*)str_header - prev_footer->len);
-
+    const Header* prev_header = from_offset_(curr_header->prev_off);
     return prev_header->str;
 }
 
 void StringList::clear() {
-    data_.clear();
-    front_ = NULL;
-    back_ = NULL;
+    memory_.clear();
+    head_off_ = 0;
+    tail_off_ = 0;
     size_ = 0;
 }
 
@@ -102,30 +123,38 @@ bool StringList::push_back(const char* str_begin, const char* str_end) {
         roc_panic("stringlist: invalid range");
     }
 
-    const size_t str_sz = (size_t)(str_end - str_begin);
-    const size_t blk_sz =
-        sizeof(Header) + AlignOps::align_as(str_sz + 1, sizeof(Header)) + sizeof(Footer);
+    const size_t str_len = size_t(str_end - str_begin);
+    const size_t blk_len =
+        sizeof(Header) + AlignOps::align_as(str_len + 1, sizeof(Header));
 
-    if (!grow_(data_.size() + blk_sz)) {
+    if (!grow_(memory_.size() + blk_len)) {
         return false;
     }
-
-    if (!data_.resize(data_.size() + blk_sz)) {
+    if (!memory_.resize(memory_.size() + blk_len)) {
         return false;
     }
 
-    front_ = (Header*)(data_.data());
-    back_ = (Header*)(data_.data() + data_.size() - blk_sz);
-    size_++;
+    const offset_t curr_off = memory_.size() - blk_len;
+    const offset_t prev_off = tail_off_;
 
-    Header* str_header = back_;
-    str_header->len = (uint32_t)blk_sz;
+    Header* curr_header = from_offset_(curr_off);
+    curr_header->prev_off = prev_off;
+    curr_header->next_off = 0;
+    curr_header->blk_len = blk_len;
 
-    memcpy(str_header->str, str_begin, str_sz); // copy string
-    str_header->str[str_sz] = '\0';             // add null
+    if (size_ != 0) {
+        Header* prev_header = from_offset_(prev_off);
+        prev_header->next_off = curr_off;
+    }
+
+    memcpy(curr_header->str, str_begin, str_len); // copy string
+    curr_header->str[str_len] = '\0';             // add null
 
-    Footer* str_footer = (Footer*)((char*)back_ + blk_sz - sizeof(Footer));
-    str_footer->len = (uint32_t)blk_sz;
+    if (size_ == 0) {
+        head_off_ = curr_off;
+    }
+    tail_off_ = curr_off;
+    size_++;
 
     return true;
 }
@@ -135,22 +164,22 @@ bool StringList::pop_back() {
         roc_panic("stringlist: list is empty");
     }
 
-    const size_t blk_sz = back_->len;
-    const Footer* prev_footer = NULL;
-    if (size_ > 1) {
-        prev_footer = (const Footer*)((const char*)back_ - sizeof(Footer));
-    }
+    Header* curr_header = from_offset_(tail_off_);
+    const offset_t prev_off = curr_header->prev_off;
 
-    if (!data_.resize(data_.size() - blk_sz)) {
+    if (!memory_.resize(memory_.size() - curr_header->blk_len)) {
         return false;
     }
 
+    if (size_ > 1) {
+        Header* prev_header = from_offset_(prev_off);
+        prev_header->next_off = 0;
+    }
+
     size_--;
-    if (size_) {
-        back_ = (Header*)(data_.data() + data_.size() - prev_footer->len);
-    } else {
-        front_ = NULL;
-        back_ = NULL;
+    tail_off_ = prev_off;
+    if (size_ == 0) {
+        head_off_ = 0;
     }
 
     return true;
@@ -170,33 +199,115 @@ const char* StringList::find(const char* str_begin, const char* str_end) {
     }
 
     if (size_ != 0) {
-        const size_t str_sz = (size_t)(str_end - str_begin);
-        const size_t blk_sz = sizeof(Header)
-            + AlignOps::align_as(str_sz + 1, sizeof(Header)) + sizeof(Footer);
+        const size_t str_len = size_t(str_end - str_begin);
+        const size_t blk_len =
+            sizeof(Header) + AlignOps::align_as(str_len + 1, sizeof(Header));
+
+        const Header* curr_header = from_offset_(head_off_);
+        const Header* back_header = from_offset_(tail_off_);
 
-        const Header* s_header = front_;
         for (;;) {
-            if (s_header->len == blk_sz
-                && memcmp(s_header->str, str_begin, str_sz) == 0) {
-                return s_header->str;
+            if (curr_header->blk_len == blk_len
+                && memcmp(curr_header->str, str_begin, str_len) == 0
+                && curr_header->str[str_len] == '\0') {
+                return curr_header->str;
             }
-            if (s_header == back_) {
+            if (curr_header == back_header) {
                 break;
             }
-            s_header = (const Header*)((const char*)s_header + s_header->len);
+            curr_header = from_offset_(curr_header->next_off);
         }
     }
 
     return NULL;
 }
 
+void StringList::sort(Order order) {
+    if (size_ < 2) {
+        return;
+    }
+
+    int (*compare)(const char* a, const char* b) =
+        order == OrderLexical ? strcmp_lexical : strcmp_natural;
+
+    for (;;) {
+        // old good bubble sort
+        bool swapped = false;
+
+        offset_t curr_off = head_off_;
+        Header* curr_header = from_offset_(curr_off);
+
+        while (curr_off != tail_off_) {
+            offset_t next_off = curr_header->next_off;
+            Header* next_header = from_offset_(next_off);
+
+            const int cmp = compare(curr_header->str, next_header->str);
+            if (cmp > 0) {
+                swap_(curr_off, curr_header, next_off, next_header);
+                swapped = true;
+            } else {
+                curr_off = next_off;
+                curr_header = next_header;
+            }
+        }
+
+        if (!swapped) {
+            break;
+        }
+    }
+}
+
+void StringList::swap_(offset_t x_off,
+                       Header* x_header,
+                       offset_t y_off,
+                       Header* y_header) {
+    offset_t prev_off = x_header->prev_off;
+    Header* prev_header = from_offset_(prev_off);
+
+    offset_t next_off = y_header->next_off;
+    Header* next_header = from_offset_(next_off);
+
+    x_header->next_off = next_off;
+    x_header->prev_off = y_off;
+
+    y_header->next_off = x_off;
+    y_header->prev_off = prev_off;
+
+    if (x_off == head_off_) {
+        head_off_ = y_off;
+    } else {
+        prev_header->next_off = y_off;
+    }
+
+    if (y_off == tail_off_) {
+        tail_off_ = x_off;
+    } else {
+        next_header->prev_off = x_off;
+    }
+}
+
+StringList::offset_t StringList::to_offset_(const Header* header) const {
+    if (!header) {
+        return 0;
+    }
+    return offset_t((const char*)header - (const char*)memory_.data());
+}
+
+const StringList::Header* StringList::from_offset_(offset_t off) const {
+    return (const Header*)(memory_.data() + off);
+}
+
+StringList::Header* StringList::from_offset_(offset_t off) {
+    return (Header*)(memory_.data() + off);
+}
+
 void StringList::check_member_(const char* str) const {
     if (size_ == 0) {
         roc_panic("stringlist: list is empty");
     }
 
-    const char* begin = &data_[0];
-    const char* end = &data_[0] + data_.size();
+    const char* begin = &memory_[0];
+    const char* end = &memory_[0] + memory_.size();
 
     if (str < begin || str >= end) {
         roc_panic("stringlist: string doesn't belong to the list");
@@ -208,7 +319,7 @@ bool StringList::grow_(size_t new_size) {
         new_size = MinCapacity;
     }
 
-    return data_.grow_exp(new_size);
+    return memory_.grow_exp(new_size);
 }
 
 } // namespace core
diff --git a/src/internal_modules/roc_core/string_list.h b/src/internal_modules/roc_core/string_list.h
index 0703ba4e6..768d269d2 100644
--- a/src/internal_modules/roc_core/string_list.h
+++ b/src/internal_modules/roc_core/string_list.h
@@ -24,14 +24,15 @@ namespace core {
 //! Dynamic list of strings.
 //!
 //! Strings are stored in a continuous dynamically-growing array.
-//! Each string is stored in a block with a header and footer,
-//! which both store block length. This allow fast iteration
-//! in both directions.
+//! Each string is stored in a block with a header which holds offsets to previous
+//! and next blocks, forming a linked list. This allows implementing bidirectional
+//! iteration and sorting. Using offsets instead of pointers is needed to avoid
+//! pointer invalidation after reallocation.
 //!
 //! @code
-//!  ++--------+--------+---------+--------++-----------
-//!  || Header | string | padding | Footer || Header ...
-//!  ++--------+--------+---------+--------++-----------
+//!  ++--------+--------+---------++-----------
+//!  || Header | string | padding || Header ...
+//!  ++--------+--------+---------++-----------
 //! @endcode
 class StringList : public NonCopyable<> {
 public:
@@ -95,31 +96,54 @@ class StringList : public NonCopyable<> {
     //! Find string in the list.
     //! @returns
     //!  the string in the list or NULL if it is not found.
-    ROC_ATTR_NODISCARD const char* find(const char* str);
+    const char* find(const char* str);
 
     //! Find string in the list.
     //! @returns
     //!  the string in the list or NULL if it is not found.
-    ROC_ATTR_NODISCARD const char* find(const char* str_begin, const char* str_end);
+    const char* find(const char* str_begin, const char* str_end);
+
+    //! String comparison algorithm.
+    enum Order {
+        //! Sort in lexicographic order.
+        //! Assumes ASCII.
+        OrderLexical,
+        //! Sort in natural order.
+        //! Assumes ASCII.
+        OrderNatural,
+    };
+
+    //! Sort list of strings according to specified order.
+    void sort(Order order);
 
 private:
     enum { MinCapacity = 128 };
 
+    typedef uint32_t offset_t;
+
     struct Header {
-        uint32_t len;
+        // offsets of next and previous elements in memory
+        offset_t next_off;
+        offset_t prev_off;
+        // len of this block, including header and padding
+        offset_t blk_len;
+        // null-terminated string
         char str[];
     };
 
-    struct Footer {
-        uint32_t len;
-    };
+    void swap_(offset_t x_off, Header* x_header, offset_t y_off, Header* y_header);
+
+    offset_t to_offset_(const Header* header) const;
+    const Header* from_offset_(offset_t off) const;
+    Header* from_offset_(offset_t off);
 
     void check_member_(const char* str) const;
+
     bool grow_(size_t size);
 
-    core::Array<char> data_;
-    Header* front_;
-    Header* back_;
+    core::Array<char> memory_;
+    offset_t head_off_;
+    offset_t tail_off_;
     size_t size_;
 };
 
diff --git a/src/internal_modules/roc_core/target_posix/roc_core/time.cpp b/src/internal_modules/roc_core/target_posix/roc_core/time.cpp
index 52e0a53bb..afebdd37b 100644
--- a/src/internal_modules/roc_core/target_posix/roc_core/time.cpp
+++ b/src/internal_modules/roc_core/target_posix/roc_core/time.cpp
@@ -49,7 +49,7 @@ nanoseconds_t timestamp(clock_t clock) {
 
 #else
 
-nanoseconds_t timestamp(clock_t) {
+nanoseconds_t timestamp(clock_t clock) {
     struct timeval tv;
     if (gettimeofday(&tv, NULL) == -1) {
         roc_panic("time: gettimeofday(): %s", errno_to_str().c_str());
@@ -77,7 +77,7 @@ void sleep_for(clock_t clock, nanoseconds_t ns) {
 
 #else
 
-void sleep_for(clock_t, nanoseconds_t ns) {
+void sleep_for(clock_t clock, nanoseconds_t ns) {
     timespec ts;
     ts.tv_sec = time_t(ns / 1000000000);
     ts.tv_nsec = long(ns % 1000000000);
diff --git a/src/internal_modules/roc_dbgio/print_supported.cpp b/src/internal_modules/roc_dbgio/print_supported.cpp
index 08d6a3f1c..cbea846c8 100644
--- a/src/internal_modules/roc_dbgio/print_supported.cpp
+++ b/src/internal_modules/roc_dbgio/print_supported.cpp
@@ -10,8 +10,8 @@
 #include "roc_address/protocol_map.h"
 #include "roc_audio/channel_defs.h"
 #include "roc_audio/channel_tables.h"
-#include "roc_audio/pcm_format.h"
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/macro_helpers.h"
 #include "roc_core/printer.h"
 #include "roc_fec/codec_map.h"
@@ -85,7 +85,7 @@ bool print_network_schemes(core::Printer& prn, core::IArena& arena) {
         }
 
         if (n_interface == 0) {
-            prn.writef("Supported schemes for network endpoints:\n");
+            prn.writef("Supported uri schemes for network endpoints:  [NET_URI]\n");
         }
 
         print_interface_protos(prn, interface_array[n_interface], list);
@@ -126,28 +126,47 @@ bool print_io_schemes(sndio::BackendDispatcher& backend_dispatcher,
         return false;
     }
 
-    prn.writef("Supported schemes for audio devices and files:\n");
+    prn.writef("Supported uri schemes for io endpoints:  [IO_URI]\n");
     prn.writef("  (--input, --output)\n");
     print_string_list(prn, list, "", "://");
 
     return true;
 }
 
-bool print_fec_schemes(core::Printer& prn, core::IArena& arena) {
-    prn.writef("Supported fec encodings:\n");
-    prn.writef("  (--fec-encoding)\n");
+bool print_network_formats(core::Printer& prn, core::IArena& arena) {
+    prn.writef("Supported formats for network packets:  [PKT_ENCODING]\n");
+    prn.writef("  (--packet-encoding)\n");
 
-    const size_t n_schemes = fec::CodecMap::instance().num_schemes();
+    prn.writef(" ");
 
-    if (n_schemes == 0) {
-        prn.writef("  none");
-    } else {
-        prn.writef("  auto");
+    for (int fmt = audio::Format_Invalid; fmt < audio::Format_Max; fmt++) {
+        if (fmt == audio::Format_Invalid) {
+            continue;
+        }
+        const audio::FormatTraits traits = audio::format_traits((audio::Format)fmt);
+        if (traits.has_flags(audio::Format_SupportsNetwork)) {
+            prn.writef(" %s", traits.name);
+        }
+    }
 
-        for (size_t n = 0; n < n_schemes; n++) {
-            prn.writef(
-                " %s",
-                packet::fec_scheme_to_str(fec::CodecMap::instance().nth_scheme(n)));
+    prn.writef("\n");
+
+    return true;
+}
+
+bool print_device_formats(core::Printer& prn, core::IArena& arena) {
+    prn.writef("Supported formats for device io:  [IO_ENCODING]\n");
+    prn.writef("  (--io-encoding)\n");
+
+    prn.writef(" ");
+
+    for (int fmt = audio::Format_Invalid; fmt < audio::Format_Max; fmt++) {
+        if (fmt == audio::Format_Invalid) {
+            continue;
+        }
+        const audio::FormatTraits traits = audio::format_traits((audio::Format)fmt);
+        if (traits.has_flags(audio::Format_SupportsDevices)) {
+            prn.writef(" %s", traits.name);
         }
     }
 
@@ -166,27 +185,30 @@ bool print_file_formats(sndio::BackendDispatcher& backend_dispatcher,
         return false;
     }
 
-    prn.writef("Supported formats for audio files:\n");
-    prn.writef("  (--input-format, --output-format)\n");
+    list.sort(core::StringList::OrderNatural);
+
+    prn.writef("Supported formats for file io:  [IO_ENCODING]\n");
+    prn.writef("  (--io-encoding)\n");
     print_string_list(prn, list, "", "");
 
     return true;
 }
 
-bool print_pcm_formats(core::Printer& prn, core::IArena& arena) {
-    prn.writef("Supported sample formats for devices, files, packets:\n");
-    prn.writef("  (--io-encoding, --packet-encoding)\n");
+bool print_pcm_subformats(core::Printer& prn, core::IArena& arena) {
+    prn.writef("Supported pcm sub-formats:"
+               "  [PKT_ENCODING, IO_ENCODING]\n");
+    prn.writef("  (--packet-encoding, --io-encoding)\n");
 
     bool first = true;
     audio::PcmTraits prev_traits, curr_traits;
 
-    for (int n = 0; n < audio::PcmFormat_Max; n++) {
-        const audio::PcmFormat fmt = (audio::PcmFormat)n;
-        if (fmt == audio::PcmFormat_Invalid) {
+    for (int n = 0; n < audio::PcmSubformat_Max; n++) {
+        const audio::PcmSubformat fmt = (audio::PcmSubformat)n;
+        if (fmt == audio::PcmSubformat_Invalid) {
             continue;
         }
 
-        curr_traits = pcm_format_traits(fmt);
+        curr_traits = pcm_subformat_traits(fmt);
 
         if (prev_traits.bit_depth != curr_traits.bit_depth
             || prev_traits.bit_width != curr_traits.bit_width) {
@@ -199,11 +221,14 @@ bool print_pcm_formats(core::Printer& prn, core::IArena& arena) {
                            (double)curr_traits.bit_width / 8.);
             }
             first = false;
+        } else if (prev_traits.has_flags(audio::Pcm_IsSigned)
+                   != curr_traits.has_flags(audio::Pcm_IsSigned)) {
+            prn.writef("  ");
         }
 
         prev_traits = curr_traits;
 
-        prn.writef(" %s", pcm_format_to_str(fmt));
+        prn.writef(" %s", pcm_subformat_to_str(fmt));
     }
 
     prn.writef("\n");
@@ -211,14 +236,58 @@ bool print_pcm_formats(core::Printer& prn, core::IArena& arena) {
     return true;
 }
 
+bool print_file_subformats(sndio::BackendDispatcher& backend_dispatcher,
+                           core::Printer& prn,
+                           core::IArena& arena) {
+    core::StringList groups(arena);
+    core::StringList subformats(arena);
+
+    if (!backend_dispatcher.get_supported_subformat_groups(groups)) {
+        return false;
+    }
+
+    bool first = true;
+
+    for (const char* grp = groups.front(); grp != NULL; grp = groups.nextof(grp)) {
+        if (first) {
+            first = false;
+        } else {
+            prn.writef("\n");
+        }
+
+        prn.writef("Supported %s sub-formats:"
+                   "  [IO_ENCODING]\n",
+                   grp);
+        prn.writef("  (--io-encoding)\n");
+
+        if (!backend_dispatcher.get_supported_subformats(grp, subformats)) {
+            return false;
+        }
+
+        subformats.sort(core::StringList::OrderNatural);
+
+        prn.writef(" ");
+
+        for (const char* subfmt = subformats.front(); subfmt != NULL;
+             subfmt = subformats.nextof(subfmt)) {
+            prn.writef(" %s", subfmt);
+        }
+
+        prn.writef("\n");
+    }
+
+    return true;
+}
+
 bool print_channel_masks(core::Printer& prn, core::IArena& arena) {
-    prn.writef("Supported channel masks for devices, files, packets:\n");
-    prn.writef("  (--io-encoding, --packet-encoding)\n");
+    prn.writef("Supported channel masks:"
+               "  [PKT_ENCODING, IO_ENCODING]\n");
+    prn.writef("  (--packet-encoding, --io-encoding)\n");
 
     for (size_t i = 0; i < ROC_ARRAY_SIZE(audio::ChanMaskNames); i++) {
         const audio::ChannelMask ch_mask = audio::ChanMaskNames[i].mask;
 
-        // TODO(gh-696): finish surround and remove this.
+        // TODO(gh-696): finish surround and enable all masks.
         if (ch_mask != audio::ChanMask_Surround_Mono
             && ch_mask != audio::ChanMask_Surround_Stereo) {
             continue;
@@ -245,7 +314,8 @@ bool print_channel_masks(core::Printer& prn, core::IArena& arena) {
 }
 
 bool print_channel_names(core::Printer& prn, core::IArena& arena) {
-    prn.writef("pre-defined channel names:\n");
+    prn.writef("Supported surround channels:"
+               "  [PKT_ENCODING, IO_ENCODING]\n");
 
     prn.writef("  front      FL FR FC\n");
     prn.writef("  side       SL SR\n");
@@ -258,6 +328,33 @@ bool print_channel_names(core::Printer& prn, core::IArena& arena) {
     return true;
 }
 
+bool print_fec_schemes(core::Printer& prn, core::IArena& arena) {
+    prn.writef("Supported fec encodings:  [FEC_ENCODING]\n");
+    prn.writef("  (--fec-encoding)\n");
+
+    const size_t n_schemes = fec::CodecMap::instance().num_schemes();
+
+    if (n_schemes == 0) {
+        prn.writef("  none");
+    } else {
+        prn.writef("  auto");
+
+        for (size_t n = 0; n < n_schemes; n++) {
+            prn.writef(
+                " %s",
+                packet::fec_scheme_to_str(fec::CodecMap::instance().nth_scheme(n)));
+        }
+    }
+
+    prn.writef("\n");
+
+    return true;
+}
+
+void print_section(core::Printer& prn, const char* section) {
+    prn.writef("[[ %s ]]\n\n", section);
+}
+
 } // namespace
 
 bool print_supported(unsigned flags,
@@ -273,6 +370,8 @@ bool print_supported(unsigned flags,
             prn.writef("\n");
         }
 
+        print_section(prn, "URI schemes");
+
         if (!print_network_schemes(prn, arena)) {
             return false;
         }
@@ -288,6 +387,32 @@ bool print_supported(unsigned flags,
         if (!print_io_schemes(backend_dispatcher, prn, arena)) {
             return false;
         }
+    }
+
+    if (flags & Print_Netio) {
+        if (first) {
+            first = false;
+        } else {
+            prn.writef("\n");
+        }
+
+        print_section(prn, "Formats");
+
+        if (!print_network_formats(prn, arena)) {
+            return false;
+        }
+    }
+
+    if (flags & Print_Sndio) {
+        if (first) {
+            first = false;
+        } else {
+            prn.writef("\n");
+        }
+
+        if (!print_device_formats(prn, arena)) {
+            return false;
+        }
 
         prn.writef("\n");
 
@@ -303,11 +428,33 @@ bool print_supported(unsigned flags,
             prn.writef("\n");
         }
 
-        if (!print_pcm_formats(prn, arena)) {
+        print_section(prn, "Sub-formats");
+
+        if (!print_pcm_subformats(prn, arena)) {
             return false;
         }
+    }
 
-        prn.writef("\n");
+    if (flags & Print_Sndio) {
+        if (first) {
+            first = false;
+        } else {
+            prn.writef("\n");
+        }
+
+        if (!print_file_subformats(backend_dispatcher, prn, arena)) {
+            return false;
+        }
+    }
+
+    if (flags & Print_Audio) {
+        if (first) {
+            first = false;
+        } else {
+            prn.writef("\n");
+        }
+
+        print_section(prn, "Channels");
 
         if (!print_channel_masks(prn, arena)) {
             return false;
@@ -321,6 +468,8 @@ bool print_supported(unsigned flags,
             prn.writef("\n");
         }
 
+        print_section(prn, "FEC");
+
         if (!print_fec_schemes(prn, arena)) {
             return false;
         }
diff --git a/src/internal_modules/roc_dbgio/target_posix/roc_dbgio/temp_file.cpp b/src/internal_modules/roc_dbgio/target_posix/roc_dbgio/temp_file.cpp
index 8f50e7bbf..6857d6148 100644
--- a/src/internal_modules/roc_dbgio/target_posix/roc_dbgio/temp_file.cpp
+++ b/src/internal_modules/roc_dbgio/target_posix/roc_dbgio/temp_file.cpp
@@ -30,7 +30,7 @@ TempFile::TempFile(const char* name) {
         tempdir = "/tmp";
     }
 
-    if (snprintf(dir_, sizeof(dir_), "%s/rocXXXXXX", tempdir) < 0) {
+    if (snprintf(dir_, sizeof(dir_), "%s/roc-XXXXXX", tempdir) < 0) {
         roc_log(LogError, "temp file: snprintf(): %s", core::errno_to_str().c_str());
         return;
     }
diff --git a/src/internal_modules/roc_pipeline/config.h b/src/internal_modules/roc_pipeline/config.h
index c0b015695..6530591de 100644
--- a/src/internal_modules/roc_pipeline/config.h
+++ b/src/internal_modules/roc_pipeline/config.h
@@ -38,7 +38,7 @@ namespace pipeline {
 
 //! Default sample specification.
 static const audio::SampleSpec DefaultSampleSpec(44100,
-                                                 audio::Sample_RawFormat,
+                                                 audio::PcmSubformat_Raw,
                                                  audio::ChanLayout_Surround,
                                                  audio::ChanOrder_Smpte,
                                                  audio::ChanMask_Surround_Stereo);
diff --git a/src/internal_modules/roc_pipeline/receiver_loop.cpp b/src/internal_modules/roc_pipeline/receiver_loop.cpp
index f26db4955..a6168f9fc 100644
--- a/src/internal_modules/roc_pipeline/receiver_loop.cpp
+++ b/src/internal_modules/roc_pipeline/receiver_loop.cpp
@@ -157,6 +157,12 @@ audio::SampleSpec ReceiverLoop::sample_spec() const {
     return source_.sample_spec();
 }
 
+core::nanoseconds_t ReceiverLoop::frame_length() const {
+    core::Mutex::Lock lock(source_mutex_);
+
+    return source_.frame_length();
+}
+
 bool ReceiverLoop::has_state() const {
     core::Mutex::Lock lock(source_mutex_);
 
diff --git a/src/internal_modules/roc_pipeline/receiver_loop.h b/src/internal_modules/roc_pipeline/receiver_loop.h
index 5faeef0f9..44cf3387b 100644
--- a/src/internal_modules/roc_pipeline/receiver_loop.h
+++ b/src/internal_modules/roc_pipeline/receiver_loop.h
@@ -147,6 +147,7 @@ class ReceiverLoop : public PipelineLoop, private sndio::ISource {
     virtual sndio::ISink* to_sink();
     virtual sndio::ISource* to_source();
     virtual audio::SampleSpec sample_spec() const;
+    virtual core::nanoseconds_t frame_length() const;
     virtual bool has_state() const;
     virtual sndio::DeviceState state() const;
     virtual status::StatusCode pause();
diff --git a/src/internal_modules/roc_pipeline/receiver_session.cpp b/src/internal_modules/roc_pipeline/receiver_session.cpp
index ac83aa646..f1c52b109 100644
--- a/src/internal_modules/roc_pipeline/receiver_session.cpp
+++ b/src/internal_modules/roc_pipeline/receiver_session.cpp
@@ -169,7 +169,7 @@ ReceiverSession::ReceiverSession(const ReceiverSessionConfig& session_config,
 
     {
         const audio::SampleSpec out_spec(pkt_encoding->sample_spec.sample_rate(),
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          pkt_encoding->sample_spec.channel_set());
 
         depacketizer_.reset(new (depacketizer_) audio::Depacketizer(
@@ -212,11 +212,11 @@ ReceiverSession::ReceiverSession(const ReceiverSessionConfig& session_config,
     if (pkt_encoding->sample_spec.channel_set()
         != common_config.output_sample_spec.channel_set()) {
         const audio::SampleSpec in_spec(pkt_encoding->sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         pkt_encoding->sample_spec.channel_set());
 
         const audio::SampleSpec out_spec(pkt_encoding->sample_spec.sample_rate(),
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          common_config.output_sample_spec.channel_set());
 
         channel_mapper_reader_.reset(
@@ -232,11 +232,11 @@ ReceiverSession::ReceiverSession(const ReceiverSessionConfig& session_config,
         || pkt_encoding->sample_spec.sample_rate()
             != common_config.output_sample_spec.sample_rate()) {
         const audio::SampleSpec in_spec(pkt_encoding->sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         common_config.output_sample_spec.channel_set());
 
         const audio::SampleSpec out_spec(common_config.output_sample_spec.sample_rate(),
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          common_config.output_sample_spec.channel_set());
 
         resampler_.reset(processor_map.new_resampler(session_config.resampler, in_spec,
@@ -259,7 +259,7 @@ ReceiverSession::ReceiverSession(const ReceiverSessionConfig& session_config,
 
     {
         const audio::SampleSpec inout_spec(
-            common_config.output_sample_spec.sample_rate(), audio::Sample_RawFormat,
+            common_config.output_sample_spec.sample_rate(), audio::PcmSubformat_Raw,
             common_config.output_sample_spec.channel_set());
 
         latency_monitor_.reset(new (latency_monitor_) audio::LatencyMonitor(
diff --git a/src/internal_modules/roc_pipeline/receiver_source.cpp b/src/internal_modules/roc_pipeline/receiver_source.cpp
index eba1253c3..21e35863c 100644
--- a/src/internal_modules/roc_pipeline/receiver_source.cpp
+++ b/src/internal_modules/roc_pipeline/receiver_source.cpp
@@ -49,7 +49,7 @@ ReceiverSource::ReceiverSource(const ReceiverSourceConfig& source_config,
     {
         const audio::SampleSpec inout_spec(
             source_config_.common.output_sample_spec.sample_rate(),
-            audio::Sample_RawFormat,
+            audio::PcmSubformat_Raw,
             source_config_.common.output_sample_spec.channel_set());
 
         mixer_.reset(new (mixer_) audio::Mixer(inout_spec, true, frame_factory_, arena));
@@ -62,7 +62,7 @@ ReceiverSource::ReceiverSource(const ReceiverSourceConfig& source_config,
     if (!source_config_.common.output_sample_spec.is_raw()) {
         const audio::SampleSpec in_spec(
             source_config_.common.output_sample_spec.sample_rate(),
-            audio::Sample_RawFormat,
+            audio::PcmSubformat_Raw,
             source_config_.common.output_sample_spec.channel_set());
 
         pcm_mapper_.reset(new (pcm_mapper_) audio::PcmMapperReader(
@@ -193,6 +193,10 @@ audio::SampleSpec ReceiverSource::sample_spec() const {
     return source_config_.common.output_sample_spec;
 }
 
+core::nanoseconds_t ReceiverSource::frame_length() const {
+    return 0;
+}
+
 bool ReceiverSource::has_state() const {
     return true;
 }
diff --git a/src/internal_modules/roc_pipeline/receiver_source.h b/src/internal_modules/roc_pipeline/receiver_source.h
index 5d6d7b1df..21185f825 100644
--- a/src/internal_modules/roc_pipeline/receiver_source.h
+++ b/src/internal_modules/roc_pipeline/receiver_source.h
@@ -88,6 +88,9 @@ class ReceiverSource : public sndio::ISource, public core::NonCopyable<> {
     //! Get sample specification of the source.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the source.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the source supports state updates.
     virtual bool has_state() const;
 
diff --git a/src/internal_modules/roc_pipeline/sender_loop.cpp b/src/internal_modules/roc_pipeline/sender_loop.cpp
index eaefb1a35..865774709 100644
--- a/src/internal_modules/roc_pipeline/sender_loop.cpp
+++ b/src/internal_modules/roc_pipeline/sender_loop.cpp
@@ -157,6 +157,12 @@ audio::SampleSpec SenderLoop::sample_spec() const {
     return sink_.sample_spec();
 }
 
+core::nanoseconds_t SenderLoop::frame_length() const {
+    core::Mutex::Lock lock(sink_mutex_);
+
+    return sink_.frame_length();
+}
+
 bool SenderLoop::has_state() const {
     core::Mutex::Lock lock(sink_mutex_);
 
diff --git a/src/internal_modules/roc_pipeline/sender_loop.h b/src/internal_modules/roc_pipeline/sender_loop.h
index 681c1b9ad..c8289d9bd 100644
--- a/src/internal_modules/roc_pipeline/sender_loop.h
+++ b/src/internal_modules/roc_pipeline/sender_loop.h
@@ -145,6 +145,7 @@ class SenderLoop : public PipelineLoop, private sndio::ISink {
     virtual sndio::ISink* to_sink();
     virtual sndio::ISource* to_source();
     virtual audio::SampleSpec sample_spec() const;
+    virtual core::nanoseconds_t frame_length() const;
     virtual bool has_state() const;
     virtual sndio::DeviceState state() const;
     virtual status::StatusCode pause();
diff --git a/src/internal_modules/roc_pipeline/sender_session.cpp b/src/internal_modules/roc_pipeline/sender_session.cpp
index d3f83faab..51a5d3c9f 100644
--- a/src/internal_modules/roc_pipeline/sender_session.cpp
+++ b/src/internal_modules/roc_pipeline/sender_session.cpp
@@ -144,7 +144,7 @@ SenderSession::create_transport_pipeline(SenderEndpoint* source_endpoint,
 
     {
         const audio::SampleSpec in_spec(pkt_encoding->sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         pkt_encoding->sample_spec.channel_set());
 
         packetizer_.reset(new (packetizer_) audio::Packetizer(
@@ -159,11 +159,11 @@ SenderSession::create_transport_pipeline(SenderEndpoint* source_endpoint,
     if (pkt_encoding->sample_spec.channel_set()
         != sink_config_.input_sample_spec.channel_set()) {
         const audio::SampleSpec in_spec(pkt_encoding->sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         sink_config_.input_sample_spec.channel_set());
 
         const audio::SampleSpec out_spec(pkt_encoding->sample_spec.sample_rate(),
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          pkt_encoding->sample_spec.channel_set());
 
         channel_mapper_writer_.reset(
@@ -179,11 +179,11 @@ SenderSession::create_transport_pipeline(SenderEndpoint* source_endpoint,
         || pkt_encoding->sample_spec.sample_rate()
             != sink_config_.input_sample_spec.sample_rate()) {
         const audio::SampleSpec in_spec(sink_config_.input_sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         sink_config_.input_sample_spec.channel_set());
 
         const audio::SampleSpec out_spec(pkt_encoding->sample_spec.sample_rate(),
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          sink_config_.input_sample_spec.channel_set());
 
         resampler_.reset(processor_map_.new_resampler(sink_config_.resampler, in_spec,
@@ -206,7 +206,7 @@ SenderSession::create_transport_pipeline(SenderEndpoint* source_endpoint,
 
     {
         const audio::SampleSpec inout_spec(sink_config_.input_sample_spec.sample_rate(),
-                                           audio::Sample_RawFormat,
+                                           audio::PcmSubformat_Raw,
                                            sink_config_.input_sample_spec.channel_set());
 
         feedback_monitor_.reset(new (feedback_monitor_) audio::FeedbackMonitor(
diff --git a/src/internal_modules/roc_pipeline/sender_sink.cpp b/src/internal_modules/roc_pipeline/sender_sink.cpp
index 419a65f7d..8ac2b8ed9 100644
--- a/src/internal_modules/roc_pipeline/sender_sink.cpp
+++ b/src/internal_modules/roc_pipeline/sender_sink.cpp
@@ -48,7 +48,7 @@ SenderSink::SenderSink(const SenderSinkConfig& sink_config,
 
     {
         const audio::SampleSpec inout_spec(sink_config_.input_sample_spec.sample_rate(),
-                                           audio::Sample_RawFormat,
+                                           audio::PcmSubformat_Raw,
                                            sink_config_.input_sample_spec.channel_set());
 
         fanout_.reset(new (fanout_) audio::Fanout(inout_spec, frame_factory_, arena_));
@@ -60,7 +60,7 @@ SenderSink::SenderSink(const SenderSinkConfig& sink_config,
 
     if (!sink_config_.input_sample_spec.is_raw()) {
         const audio::SampleSpec out_spec(sink_config_.input_sample_spec.sample_rate(),
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          sink_config_.input_sample_spec.channel_set());
 
         pcm_mapper_.reset(new (pcm_mapper_) audio::PcmMapperWriter(
@@ -192,6 +192,10 @@ audio::SampleSpec SenderSink::sample_spec() const {
     return sink_config_.input_sample_spec;
 }
 
+core::nanoseconds_t SenderSink::frame_length() const {
+    return 0;
+}
+
 bool SenderSink::has_state() const {
     return true;
 }
diff --git a/src/internal_modules/roc_pipeline/sender_sink.h b/src/internal_modules/roc_pipeline/sender_sink.h
index 6bef71e97..37290ad4f 100644
--- a/src/internal_modules/roc_pipeline/sender_sink.h
+++ b/src/internal_modules/roc_pipeline/sender_sink.h
@@ -87,6 +87,9 @@ class SenderSink : public sndio::ISink, public core::NonCopyable<> {
     //! Get sample specification of the sink.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the sink.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the sink supports state updates.
     virtual bool has_state() const;
 
diff --git a/src/internal_modules/roc_pipeline/transcoder_sink.cpp b/src/internal_modules/roc_pipeline/transcoder_sink.cpp
index 4109f6257..932bd89f1 100644
--- a/src/internal_modules/roc_pipeline/transcoder_sink.cpp
+++ b/src/internal_modules/roc_pipeline/transcoder_sink.cpp
@@ -39,11 +39,11 @@ TranscoderSink::TranscoderSink(const TranscoderConfig& config,
     if (config_.input_sample_spec.channel_set()
         != config_.output_sample_spec.channel_set()) {
         const audio::SampleSpec from_spec(config_.output_sample_spec.sample_rate(),
-                                          audio::Sample_RawFormat,
+                                          audio::PcmSubformat_Raw,
                                           config_.input_sample_spec.channel_set());
 
         const audio::SampleSpec to_spec(config_.output_sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         config_.output_sample_spec.channel_set());
 
         channel_mapper_writer_.reset(
@@ -58,11 +58,11 @@ TranscoderSink::TranscoderSink(const TranscoderConfig& config,
     if (config_.input_sample_spec.sample_rate()
         != config_.output_sample_spec.sample_rate()) {
         const audio::SampleSpec from_spec(config_.input_sample_spec.sample_rate(),
-                                          audio::Sample_RawFormat,
+                                          audio::PcmSubformat_Raw,
                                           config_.input_sample_spec.channel_set());
 
         const audio::SampleSpec to_spec(config_.output_sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         config_.input_sample_spec.channel_set());
 
         resampler_.reset(processor_map.new_resampler(config_.resampler, from_spec,
@@ -116,6 +116,10 @@ audio::SampleSpec TranscoderSink::sample_spec() const {
     return config_.output_sample_spec;
 }
 
+core::nanoseconds_t TranscoderSink::frame_length() const {
+    return 0;
+}
+
 bool TranscoderSink::has_state() const {
     return false;
 }
diff --git a/src/internal_modules/roc_pipeline/transcoder_sink.h b/src/internal_modules/roc_pipeline/transcoder_sink.h
index 6a4388207..ffe991cc9 100644
--- a/src/internal_modules/roc_pipeline/transcoder_sink.h
+++ b/src/internal_modules/roc_pipeline/transcoder_sink.h
@@ -57,6 +57,9 @@ class TranscoderSink : public sndio::ISink, public core::NonCopyable<> {
     //! Get sample specification of the sink.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the sink.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the sink supports state updates.
     virtual bool has_state() const;
 
diff --git a/src/internal_modules/roc_pipeline/transcoder_source.cpp b/src/internal_modules/roc_pipeline/transcoder_source.cpp
index 6ee9f6e73..719344db7 100644
--- a/src/internal_modules/roc_pipeline/transcoder_source.cpp
+++ b/src/internal_modules/roc_pipeline/transcoder_source.cpp
@@ -37,11 +37,11 @@ TranscoderSource::TranscoderSource(const TranscoderConfig& config,
     if (config_.input_sample_spec.channel_set()
         != config_.output_sample_spec.channel_set()) {
         const audio::SampleSpec from_spec(config_.input_sample_spec.sample_rate(),
-                                          audio::Sample_RawFormat,
+                                          audio::PcmSubformat_Raw,
                                           config_.input_sample_spec.channel_set());
 
         const audio::SampleSpec to_spec(config_.input_sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         config_.output_sample_spec.channel_set());
 
         channel_mapper_reader_.reset(
@@ -56,11 +56,11 @@ TranscoderSource::TranscoderSource(const TranscoderConfig& config,
     if (config_.input_sample_spec.sample_rate()
         != config_.output_sample_spec.sample_rate()) {
         const audio::SampleSpec from_spec(config_.input_sample_spec.sample_rate(),
-                                          audio::Sample_RawFormat,
+                                          audio::PcmSubformat_Raw,
                                           config_.output_sample_spec.channel_set());
 
         const audio::SampleSpec to_spec(config_.output_sample_spec.sample_rate(),
-                                        audio::Sample_RawFormat,
+                                        audio::PcmSubformat_Raw,
                                         config_.output_sample_spec.channel_set());
 
         resampler_.reset(processor_map.new_resampler(config_.resampler, from_spec,
@@ -114,6 +114,10 @@ audio::SampleSpec TranscoderSource::sample_spec() const {
     return config_.output_sample_spec;
 }
 
+core::nanoseconds_t TranscoderSource::frame_length() const {
+    return 0;
+}
+
 bool TranscoderSource::has_state() const {
     return input_source_.has_state();
 }
diff --git a/src/internal_modules/roc_pipeline/transcoder_source.h b/src/internal_modules/roc_pipeline/transcoder_source.h
index a34d82f27..cd62c1545 100644
--- a/src/internal_modules/roc_pipeline/transcoder_source.h
+++ b/src/internal_modules/roc_pipeline/transcoder_source.h
@@ -56,6 +56,9 @@ class TranscoderSource : public sndio::ISource, public core::NonCopyable<> {
     //! Get sample specification of the source.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the source.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the source supports state updates.
     virtual bool has_state() const;
 
diff --git a/src/internal_modules/roc_rtp/encoding.cpp b/src/internal_modules/roc_rtp/encoding.cpp
index 1f84f7e1b..277ea83e4 100644
--- a/src/internal_modules/roc_rtp/encoding.cpp
+++ b/src/internal_modules/roc_rtp/encoding.cpp
@@ -24,14 +24,16 @@ bool parse_encoding(const char* str, Encoding& result) {
     if (sep == NULL) {
         roc_log(LogError,
                 "parse encoding: invalid format: missing separator, expected"
-                " <id>:<format>/<rate>/<channels>");
+                " '<id>:<spec>', got '%s'",
+                str);
         return false;
     }
 
     if (!isdigit(*str)) {
         roc_log(LogError,
-                "parse encoding: invalid format: not a number, expected"
-                " <id>:<format>/<rate>/<channels>");
+                "parse encoding: invalid id: not a number, expected"
+                " '<id>:<spec>', got '%s'",
+                str);
         return false;
     }
 
@@ -40,23 +42,22 @@ bool parse_encoding(const char* str, Encoding& result) {
 
     if (number == ULONG_MAX || !number_end || number_end != sep) {
         roc_log(LogError,
-                "parse encoding: invalid format: not a number, expected"
-                " <id>:<format>/<rate>/<channels>");
+                "parse encoding: invalid id: not a number, expected"
+                " '<id>:<spec>', got '%s'",
+                str);
         return false;
     }
 
     if (number > UINT_MAX) {
         roc_log(LogError,
-                "parse encoding: number out of range:"
-                " value=%lu maximum=%u",
+                "parse encoding: invalid id: out of range:"
+                " got=%lu max=%u",
                 number, UINT_MAX);
         return false;
     }
 
     if (!audio::parse_sample_spec(sep + 1, result.sample_spec)) {
-        roc_log(LogError,
-                "parse encoding: invalid format: invalid spec, expected"
-                " <id>:<format>/<rate>/<channels>");
+        roc_log(LogError, "parse encoding: invalid spec");
         return false;
     }
 
diff --git a/src/internal_modules/roc_rtp/encoding.h b/src/internal_modules/roc_rtp/encoding.h
index 373ffeb81..8fbf33c3a 100644
--- a/src/internal_modules/roc_rtp/encoding.h
+++ b/src/internal_modules/roc_rtp/encoding.h
@@ -14,7 +14,7 @@
 
 #include "roc_audio/iframe_decoder.h"
 #include "roc_audio/iframe_encoder.h"
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_audio/sample_spec.h"
 #include "roc_core/attributes.h"
 #include "roc_core/iarena.h"
diff --git a/src/internal_modules/roc_rtp/encoding_map.cpp b/src/internal_modules/roc_rtp/encoding_map.cpp
index 1e63d8bf2..1f1557601 100644
--- a/src/internal_modules/roc_rtp/encoding_map.cpp
+++ b/src/internal_modules/roc_rtp/encoding_map.cpp
@@ -7,9 +7,9 @@
  */
 
 #include "roc_rtp/encoding_map.h"
+#include "roc_audio/format.h"
 #include "roc_audio/pcm_decoder.h"
 #include "roc_audio/pcm_encoder.h"
-#include "roc_audio/sample_format.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/panic.h"
 #include "roc_status/code_to_str.h"
@@ -24,7 +24,7 @@ EncodingMap::EncodingMap(core::IArena& arena)
         Encoding enc;
         enc.payload_type = PayloadType_L16_Mono;
         enc.sample_spec = audio::SampleSpec(
-            44100, audio::PcmFormat_SInt16_Be, audio::ChanLayout_Surround,
+            44100, audio::PcmSubformat_SInt16_Be, audio::ChanLayout_Surround,
             audio::ChanOrder_Smpte, audio::ChanMask_Surround_Mono);
         enc.packet_flags = packet::Packet::FlagAudio;
 
@@ -34,7 +34,7 @@ EncodingMap::EncodingMap(core::IArena& arena)
         Encoding enc;
         enc.payload_type = PayloadType_L16_Stereo;
         enc.sample_spec = audio::SampleSpec(
-            44100, audio::PcmFormat_SInt16_Be, audio::ChanLayout_Surround,
+            44100, audio::PcmSubformat_SInt16_Be, audio::ChanLayout_Surround,
             audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo);
         enc.packet_flags = packet::Packet::FlagAudio;
 
@@ -80,7 +80,36 @@ status::StatusCode EncodingMap::register_encoding(Encoding enc) {
         return status::StatusBadArg;
     }
 
-    if (!enc.sample_spec.is_valid()) {
+    if (enc.sample_spec.format() == audio::Format_Invalid) {
+        roc_log(LogError,
+                "encoding map: failed to register encoding:"
+                " missing format");
+        return status::StatusBadArg;
+    }
+
+    if (enc.sample_spec.format() == audio::Format_Pcm
+        && enc.sample_spec.pcm_subformat() == audio::PcmSubformat_Invalid) {
+        roc_log(LogError,
+                "encoding map: failed to register encoding:"
+                " missing sub-format");
+        return status::StatusBadArg;
+    }
+
+    if (enc.sample_spec.sample_rate() == 0) {
+        roc_log(LogError,
+                "encoding map: failed to register encoding:"
+                " missing rate");
+        return status::StatusBadArg;
+    }
+
+    if (!enc.sample_spec.channel_set().is_valid()) {
+        roc_log(LogError,
+                "encoding map: failed to register encoding:"
+                " missing channels");
+        return status::StatusBadArg;
+    }
+
+    if (!enc.sample_spec.is_complete()) {
         roc_log(LogError,
                 "encoding map: failed to register encoding:"
                 " invalid encoding parameters");
@@ -133,8 +162,8 @@ void EncodingMap::resolve_codecs_(Encoding& enc) {
         return;
     }
 
-    switch (enc.sample_spec.sample_format()) {
-    case audio::SampleFormat_Pcm:
+    switch (enc.sample_spec.format()) {
+    case audio::Format_Pcm:
         if (!enc.new_encoder) {
             enc.new_encoder = &audio::PcmEncoder::construct;
         }
@@ -143,7 +172,7 @@ void EncodingMap::resolve_codecs_(Encoding& enc) {
         }
         break;
 
-    case audio::SampleFormat_Invalid:
+    default:
         break;
     }
 }
diff --git a/src/internal_modules/roc_sndio/backend_dispatcher.cpp b/src/internal_modules/roc_sndio/backend_dispatcher.cpp
index d0d2026d2..be1ed4de0 100644
--- a/src/internal_modules/roc_sndio/backend_dispatcher.cpp
+++ b/src/internal_modules/roc_sndio/backend_dispatcher.cpp
@@ -17,41 +17,28 @@ namespace sndio {
 
 namespace {
 
-DriverType select_driver_type(const address::IoUri& uri) {
-    if (uri.is_file()) {
-        return DriverType_File;
-    } else {
-        return DriverType_Device;
+bool match_driver(const DriverInfo& driver_info,
+                  unsigned driver_flags,
+                  const char* driver_name) {
+    if (driver_name && strcmp(driver_info.driver_name, driver_name) != 0) {
+        return false;
     }
-}
 
-const char* select_driver_name(const address::IoUri& uri, const char* force_format) {
-    if (uri.is_file()) {
-        if (force_format && *force_format) {
-            // use specific file driver
-            return force_format;
-        }
-        // auto-detect file driver
-        return NULL;
+    if ((driver_info.driver_flags & driver_flags) != driver_flags) {
+        return false;
     }
 
-    // use specific device driver
-    return uri.scheme();
+    return true;
 }
 
-bool match_driver(const DriverInfo& driver_info,
-                  const char* driver_name,
-                  DriverType driver_type,
-                  unsigned driver_flags) {
-    if (driver_name != NULL && strcmp(driver_info.name, driver_name) != 0) {
-        return false;
-    }
-
-    if (driver_info.type != driver_type) {
+bool match_format(const FormatInfo& format_info,
+                  unsigned driver_flags,
+                  const char* format_name) {
+    if (format_name && strcmp(format_info.format_name, format_name) != 0) {
         return false;
     }
 
-    if ((driver_info.flags & driver_flags) == 0) {
+    if ((format_info.driver_flags & driver_flags) != driver_flags) {
         return false;
     }
 
@@ -104,20 +91,19 @@ BackendDispatcher::open_default_source(const IoConfig& io_config,
 }
 
 status::StatusCode BackendDispatcher::open_sink(const address::IoUri& uri,
-                                                const char* force_format,
                                                 const IoConfig& io_config,
                                                 core::ScopedPtr<ISink>& result) {
     if (!uri.is_valid()) {
         roc_panic("backend dispatcher: invalid uri");
     }
 
-    const DriverType driver_type = select_driver_type(uri);
-    const char* driver_name = select_driver_name(uri, force_format);
+    const char* driver = uri.scheme();
+    const char* path = uri.path();
 
     IDevice* device = NULL;
 
-    const status::StatusCode code = open_device_(
-        DeviceType_Sink, driver_type, driver_name, uri.path(), io_config, &device);
+    const status::StatusCode code =
+        open_file_or_device_(DeviceType_Sink, driver, path, io_config, &device);
     if (code != status::StatusOK) {
         return code;
     }
@@ -130,20 +116,19 @@ status::StatusCode BackendDispatcher::open_sink(const address::IoUri& uri,
 }
 
 status::StatusCode BackendDispatcher::open_source(const address::IoUri& uri,
-                                                  const char* force_format,
                                                   const IoConfig& io_config,
                                                   core::ScopedPtr<ISource>& result) {
     if (!uri.is_valid()) {
         roc_panic("backend dispatcher: invalid uri");
     }
 
-    const DriverType driver_type = select_driver_type(uri);
-    const char* driver_name = select_driver_name(uri, force_format);
+    const char* driver = uri.scheme();
+    const char* path = uri.path();
 
     IDevice* device = NULL;
 
-    const status::StatusCode code = open_device_(
-        DeviceType_Source, driver_type, driver_name, uri.path(), io_config, &device);
+    const status::StatusCode code =
+        open_file_or_device_(DeviceType_Source, driver, path, io_config, &device);
     if (code != status::StatusOK) {
         return code;
     }
@@ -162,36 +147,24 @@ bool BackendDispatcher::get_supported_schemes(core::StringList& result) {
     for (size_t n = 0; n < BackendMap::instance().num_drivers(); n++) {
         const DriverInfo& driver_info = BackendMap::instance().nth_driver(n);
 
-        // every device driver has its own scheme
-        if (driver_info.type == DriverType_Device) {
-            if (result.find(driver_info.name)) {
-                continue;
-            }
-            if (!result.push_back(driver_info.name)) {
+        if (!result.find(driver_info.driver_name)) {
+            if (!result.push_back(driver_info.driver_name)) {
                 return false;
             }
         }
     }
 
-    // all file drivers has a single "file" scheme
-    if (!result.push_back("file")) {
-        return false;
-    }
-
     return true;
 }
 
 bool BackendDispatcher::get_supported_formats(core::StringList& result) {
     result.clear();
 
-    for (size_t n = 0; n < BackendMap::instance().num_drivers(); n++) {
-        const DriverInfo& driver_info = BackendMap::instance().nth_driver(n);
+    for (size_t n = 0; n < BackendMap::instance().num_formats(); n++) {
+        const FormatInfo& format_info = BackendMap::instance().nth_format(n);
 
-        if (driver_info.type == DriverType_File) {
-            if (result.find(driver_info.name)) {
-                continue;
-            }
-            if (!result.push_back(driver_info.name)) {
+        if (!result.find(format_info.format_name)) {
+            if (!result.push_back(format_info.format_name)) {
                 return false;
             }
         }
@@ -200,105 +173,209 @@ bool BackendDispatcher::get_supported_formats(core::StringList& result) {
     return true;
 }
 
+bool BackendDispatcher::get_supported_subformat_groups(core::StringList& result) {
+    result.clear();
+
+    for (size_t n = 0; n < BackendMap::instance().num_backends(); n++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n);
+
+        if (!backend.discover_subformat_groups(result)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool BackendDispatcher::get_supported_subformats(const char* group,
+                                                 core::StringList& result) {
+    result.clear();
+
+    for (size_t n = 0; n < BackendMap::instance().num_backends(); n++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n);
+
+        if (!backend.discover_subformats(group, result)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
 status::StatusCode BackendDispatcher::open_default_device_(DeviceType device_type,
                                                            const IoConfig& io_config,
                                                            IDevice** result) {
-    const unsigned driver_flags =
-        unsigned(DriverFlag_IsDefault
-                 | (device_type == DeviceType_Sink ? DriverFlag_SupportsSink
-                                                   : DriverFlag_SupportsSource));
+    roc_panic_if(!result);
 
     status::StatusCode code = status::StatusNoDriver;
 
+    // Try all drivers with Driver_DefaultDevice flag.
+    const unsigned driver_flags = Driver_Device | Driver_DefaultDevice
+        | (device_type == DeviceType_Sink ? Driver_SupportsSink : Driver_SupportsSource);
+
     for (size_t n = 0; n < BackendMap::instance().num_drivers(); n++) {
         const DriverInfo& driver_info = BackendMap::instance().nth_driver(n);
 
-        if (!match_driver(driver_info, NULL, DriverType_Device, driver_flags)) {
+        if (!match_driver(driver_info, driver_flags, NULL)) {
             continue;
         }
 
-        code = driver_info.backend->open_device(device_type, DriverType_Device,
-                                                driver_info.name, "default", io_config,
-                                                frame_factory_, arena_, result);
+        code = driver_info.backend->open_device(device_type, driver_info.driver_name,
+                                                "default", io_config, frame_factory_,
+                                                arena_, result);
 
         if (code == status::StatusOK) {
             return code;
         }
 
-        if (code != status::StatusNoDriver) {
-            roc_log(LogDebug,
-                    "backend dispatcher: got error from driver:"
-                    " driver=%s status=%s",
-                    driver_info.name, status::code_to_str(code));
+        if (code == status::StatusNoDriver) {
+            continue;
         }
+
+        break;
     }
 
-    roc_log(LogError, "backend dispatcher: failed to open default %s: status=%s",
-            device_type_to_str(device_type), status::code_to_str(code));
+    roc_log(LogError, "backend dispatcher: failed to open default device: status=%s",
+            status::code_to_str(code));
 
     return code;
 }
 
+status::StatusCode BackendDispatcher::open_file_or_device_(DeviceType device_type,
+                                                           const char* driver,
+                                                           const char* path,
+                                                           const IoConfig& io_config,
+                                                           IDevice** result) {
+    roc_panic_if(!driver);
+    roc_panic_if(!path);
+    roc_panic_if(!result);
+
+    if (strcmp(driver, "file") == 0) {
+        if (io_config.latency != 0) {
+            roc_log(LogError,
+                    "backend dispatcher: it's not possible to specify io latency"
+                    " for files");
+            return status::StatusBadConfig;
+        }
+
+        if (device_type == DeviceType_Sink && strcmp(path, "-") == 0
+            && !io_config.sample_spec.has_format()) {
+            roc_log(
+                LogError,
+                "backend dispatcher: when output file is \"-\", format must be specified"
+                " explicitly via io encoding");
+            return status::StatusBadConfig;
+        }
+
+        return open_file_(device_type, driver, path, io_config, result);
+    }
+
+    return open_device_(device_type, driver, path, io_config, result);
+}
+
 status::StatusCode BackendDispatcher::open_device_(DeviceType device_type,
-                                                   DriverType driver_type,
-                                                   const char* driver_name,
+                                                   const char* driver,
                                                    const char* path,
                                                    const IoConfig& io_config,
                                                    IDevice** result) {
-    const unsigned driver_flags =
-        (device_type == DeviceType_Sink ? DriverFlag_SupportsSink
-                                        : DriverFlag_SupportsSource);
+    status::StatusCode code = status::StatusNoDriver;
+
+    const unsigned driver_flags = Driver_Device
+        | (device_type == DeviceType_Sink ? Driver_SupportsSink : Driver_SupportsSource);
+
+    // We're opening device, driver defines device type (pulseaudio, alsa, etc).
+    // Try backends which support matching driver.
+    for (size_t n = 0; n < BackendMap::instance().num_drivers(); n++) {
+        const DriverInfo& driver_info = BackendMap::instance().nth_driver(n);
 
+        if (!match_driver(driver_info, driver_flags, driver)) {
+            continue;
+        }
+
+        code = driver_info.backend->open_device(device_type, driver, path, io_config,
+                                                frame_factory_, arena_, result);
+
+        if (code == status::StatusOK) {
+            return code;
+        }
+
+        if (code == status::StatusNoDriver) {
+            // No error, backend just doesn't support driver.
+            continue;
+        }
+
+        break;
+    }
+
+    roc_log(LogError,
+            "backend dispatcher: failed to open device:"
+            " device_type=%s driver=%s path=%s status=%s",
+            device_type_to_str(device_type), driver, path, status::code_to_str(code));
+
+    return code;
+}
+
+status::StatusCode BackendDispatcher::open_file_(DeviceType device_type,
+                                                 const char* driver,
+                                                 const char* path,
+                                                 const IoConfig& io_config,
+                                                 IDevice** result) {
     status::StatusCode code = status::StatusNoDriver;
 
-    if (driver_name != NULL) {
-        for (size_t n = 0; n < BackendMap::instance().num_drivers(); n++) {
-            const DriverInfo& driver_info = BackendMap::instance().nth_driver(n);
+    const unsigned driver_flags = Driver_File
+        | (device_type == DeviceType_Sink ? Driver_SupportsSink : Driver_SupportsSource);
 
-            if (!match_driver(driver_info, driver_name, driver_type, driver_flags)) {
+    if (io_config.sample_spec.has_format()) {
+        // We're opening file and format is specified explicitly (wav, flac, etc).
+        // Try backends which support requested format.
+        for (size_t n = 0; n < BackendMap::instance().num_formats(); n++) {
+            const FormatInfo& format_info = BackendMap::instance().nth_format(n);
+
+            if (!match_format(format_info, driver_flags,
+                              io_config.sample_spec.format_name())) {
                 continue;
             }
 
-            code = driver_info.backend->open_device(device_type, driver_type,
-                                                    driver_info.name, path, io_config,
+            code = format_info.backend->open_device(device_type, driver, path, io_config,
                                                     frame_factory_, arena_, result);
 
             if (code == status::StatusOK) {
                 return code;
             }
 
-            if (code != status::StatusNoDriver) {
-                roc_log(LogDebug,
-                        "backend dispatcher: got error from driver:"
-                        " driver=%s status=%s",
-                        driver_info.name, status::code_to_str(code));
+            if (code == status::StatusNoDriver || code == status::StatusNoFormat) {
+                // No error, backend just doesn't support driver or format.
+                continue;
             }
+
+            break;
         }
     } else {
+        // We're opening file and format is omitted.
+        // Try all backends.
         for (size_t n = 0; n < BackendMap::instance().num_backends(); n++) {
             IBackend& backend = BackendMap::instance().nth_backend(n);
 
-            code = backend.open_device(device_type, driver_type, NULL, path, io_config,
+            code = backend.open_device(device_type, driver, path, io_config,
                                        frame_factory_, arena_, result);
 
             if (code == status::StatusOK) {
                 return code;
             }
 
-            if (code != status::StatusNoDriver) {
-                roc_log(LogDebug,
-                        "backend dispatcher: got error from backend:"
-                        " backend=%s status=%s",
-                        backend.name(), status::code_to_str(code));
+            if (code == status::StatusNoDriver || code == status::StatusNoFormat) {
+                // No error, backend just doesn't support driver or format.
+                continue;
             }
+
+            break;
         }
     }
 
     roc_log(LogError,
-            "backend dispatcher: failed to open %s:"
-            " driver_type=%s driver_name=%s path=%s status=%s",
-            device_type_to_str(device_type), driver_type_to_str(driver_type), driver_name,
-            path, status::code_to_str(code));
+            "backend dispatcher: failed to open file:"
+            " device_type=%s driver=%s path=%s status=%s",
+            device_type_to_str(device_type), driver, path, status::code_to_str(code));
 
     return code;
 }
diff --git a/src/internal_modules/roc_sndio/backend_dispatcher.h b/src/internal_modules/roc_sndio/backend_dispatcher.h
index 002ed0453..a4b4df4e9 100644
--- a/src/internal_modules/roc_sndio/backend_dispatcher.h
+++ b/src/internal_modules/roc_sndio/backend_dispatcher.h
@@ -47,13 +47,11 @@ class BackendDispatcher : public core::NonCopyable<> {
 
     //! Create and open a sink.
     ROC_ATTR_NODISCARD status::StatusCode open_sink(const address::IoUri& uri,
-                                                    const char* force_format,
                                                     const IoConfig& io_config,
                                                     core::ScopedPtr<ISink>& result);
 
     //! Create and open a source.
     ROC_ATTR_NODISCARD status::StatusCode open_source(const address::IoUri& uri,
-                                                      const char* force_format,
                                                       const IoConfig& io_config,
                                                       core::ScopedPtr<ISource>& result);
 
@@ -63,18 +61,36 @@ class BackendDispatcher : public core::NonCopyable<> {
     //! Get all supported file formats.
     ROC_ATTR_NODISCARD bool get_supported_formats(core::StringList& result);
 
+    //! Get all groups of sub-formats.
+    ROC_ATTR_NODISCARD bool get_supported_subformat_groups(core::StringList& result);
+
+    //! Get all sub-formats in group.
+    ROC_ATTR_NODISCARD bool get_supported_subformats(const char* group,
+                                                     core::StringList& result);
+
 private:
     status::StatusCode open_default_device_(DeviceType device_type,
                                             const IoConfig& io_config,
                                             IDevice** result);
 
+    status::StatusCode open_file_or_device_(DeviceType device_type,
+                                            const char* driver,
+                                            const char* path,
+                                            const IoConfig& io_config,
+                                            IDevice** result);
+
     status::StatusCode open_device_(DeviceType device_type,
-                                    DriverType driver_type,
-                                    const char* driver_name,
+                                    const char* driver,
                                     const char* path,
                                     const IoConfig& io_config,
                                     IDevice** result);
 
+    status::StatusCode open_file_(DeviceType device_type,
+                                  const char* driver,
+                                  const char* path,
+                                  const IoConfig& io_config,
+                                  IDevice** result);
+
     audio::FrameFactory frame_factory_;
     core::IArena& arena_;
 };
diff --git a/src/internal_modules/roc_sndio/backend_map.cpp b/src/internal_modules/roc_sndio/backend_map.cpp
index a55d3b00f..ab4f1858e 100644
--- a/src/internal_modules/roc_sndio/backend_map.cpp
+++ b/src/internal_modules/roc_sndio/backend_map.cpp
@@ -15,12 +15,15 @@ namespace sndio {
 
 BackendMap::BackendMap()
     : backends_(core::NoopArena)
-    , drivers_(core::NoopArena) {
+    , drivers_(core::NoopArena)
+    , formats_(core::NoopArena) {
     register_backends_();
-    register_drivers_();
+    collect_drivers_();
+    collect_formats_();
 
-    roc_log(LogDebug, "backend map: initializing: n_backends=%d n_drivers=%d",
-            (int)backends_.size(), (int)drivers_.size());
+    roc_log(LogDebug,
+            "backend map: initializing: n_backends=%d n_drivers=%d n_formats=%d",
+            (int)backends_.size(), (int)drivers_.size(), (int)formats_.size());
 }
 
 size_t BackendMap::num_backends() const {
@@ -39,6 +42,14 @@ const DriverInfo& BackendMap::nth_driver(size_t driver_index) const {
     return drivers_[driver_index];
 }
 
+size_t BackendMap::num_formats() const {
+    return formats_.size();
+}
+
+const FormatInfo& BackendMap::nth_format(size_t format_index) const {
+    return formats_[format_index];
+}
+
 void BackendMap::register_backends_() {
 #ifdef ROC_TARGET_PULSEAUDIO
     pulseaudio_backend_.reset(new (pulseaudio_backend_) PulseaudioBackend);
@@ -59,15 +70,25 @@ void BackendMap::register_backends_() {
 #endif // ROC_TARGET_SOX
 }
 
-void BackendMap::register_drivers_() {
+void BackendMap::add_backend_(IBackend* backend) {
+    if (!backends_.push_back(backend)) {
+        roc_panic("backend map: can't register backend");
+    }
+}
+
+void BackendMap::collect_drivers_() {
     for (size_t n = 0; n < backends_.size(); n++) {
-        backends_[n]->discover_drivers(drivers_);
+        if (!backends_[n]->discover_drivers(drivers_)) {
+            roc_panic("backend map: can't register driver");
+        }
     }
 }
 
-void BackendMap::add_backend_(IBackend* backend) {
-    if (!backends_.push_back(backend)) {
-        roc_panic("backend map: can't register backend");
+void BackendMap::collect_formats_() {
+    for (size_t n = 0; n < backends_.size(); n++) {
+        if (!backends_[n]->discover_formats(formats_)) {
+            roc_panic("backend map: can't register format");
+        }
     }
 }
 
diff --git a/src/internal_modules/roc_sndio/backend_map.h b/src/internal_modules/roc_sndio/backend_map.h
index 766a28f78..a9fe05ac6 100644
--- a/src/internal_modules/roc_sndio/backend_map.h
+++ b/src/internal_modules/roc_sndio/backend_map.h
@@ -55,16 +55,23 @@ class BackendMap : public core::NonCopyable<> {
     //! Get driver by index.
     const DriverInfo& nth_driver(size_t driver_index) const;
 
+    //! Get number of file formats available.
+    size_t num_formats() const;
+
+    //! Get driver by index.
+    const FormatInfo& nth_format(size_t format_index) const;
+
 private:
     friend class core::Singleton<BackendMap>;
 
     BackendMap();
 
     void register_backends_();
-    void register_drivers_();
-
     void add_backend_(IBackend*);
 
+    void collect_drivers_();
+    void collect_formats_();
+
 #ifdef ROC_TARGET_PULSEAUDIO
     core::Optional<PulseaudioBackend> pulseaudio_backend_;
 #endif // ROC_TARGET_PULSEAUDIO
@@ -81,6 +88,7 @@ class BackendMap : public core::NonCopyable<> {
 
     core::Array<IBackend*, MaxBackends> backends_;
     core::Array<DriverInfo, MaxDrivers> drivers_;
+    core::Array<FormatInfo, MaxFormats> formats_;
 };
 
 } // namespace sndio
diff --git a/src/internal_modules/roc_sndio/driver.cpp b/src/internal_modules/roc_sndio/driver.cpp
deleted file mode 100644
index 463267f8c..000000000
--- a/src/internal_modules/roc_sndio/driver.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (c) 2022 Roc authors
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-#include "roc_sndio/driver.h"
-
-namespace roc {
-namespace sndio {
-
-const char* driver_type_to_str(DriverType type) {
-    switch (type) {
-    case DriverType_Device:
-        return "device";
-
-    case DriverType_File:
-        return "file";
-
-    case DriverType_Invalid:
-    default:
-        break;
-    }
-
-    return "<invalid>";
-}
-
-} // namespace sndio
-} // namespace roc
diff --git a/src/internal_modules/roc_sndio/driver.h b/src/internal_modules/roc_sndio/driver.h
index ff2406f99..2ba4422a0 100644
--- a/src/internal_modules/roc_sndio/driver.h
+++ b/src/internal_modules/roc_sndio/driver.h
@@ -7,7 +7,7 @@
  */
 
 //! @file roc_sndio/driver.h
-//! @brief Driver types.
+//! @brief Driver information.
 
 #ifndef ROC_SNDIO_DRIVER_H_
 #define ROC_SNDIO_DRIVER_H_
@@ -21,71 +21,96 @@ namespace sndio {
 class IBackend;
 
 //! Maximum number of drivers.
-static const size_t MaxDrivers = 128;
+static const size_t MaxDrivers = 16;
 
-//! Driver type.
-enum DriverType {
-    //! Invalid type.
-    DriverType_Invalid,
-
-    //! Driver for audio files.
-    DriverType_File,
-
-    //! Driver for audio devices.
-    DriverType_Device
-};
+//! Maximum number of file formats.
+static const size_t MaxFormats = 128;
 
 //! Driver flags.
 enum DriverFlags {
+    //! This is driver for audio files.
+    Driver_File = (1 << 0),
+
+    //! This is driver for audio devices.
+    Driver_Device = (1 << 1),
+
     //! Driver is used if no file or device is specified.
-    DriverFlag_IsDefault = (1 << 0),
+    Driver_DefaultDevice = (1 << 2),
 
     //! Driver supports sources (input).
-    DriverFlag_SupportsSource = (1 << 1),
+    Driver_SupportsSource = (1 << 3),
 
     //! Driver supports sinks (output).
-    DriverFlag_SupportsSink = (1 << 2)
+    Driver_SupportsSink = (1 << 4)
 };
 
-//! Driver information.
+//! Information about driver.
 struct DriverInfo {
     //! Driver name.
-    char name[20];
-
-    //! Driver type.
-    DriverType type;
+    char driver_name[12];
 
     //! Driver flags.
-    unsigned int flags;
+    unsigned int driver_flags;
 
-    //! Backend the driver uses.
+    //! Associated backend.
     IBackend* backend;
 
     //! Initialize.
-    DriverInfo()
-        : type(DriverType_Invalid)
-        , flags(0)
-        , backend(NULL) {
-        strcpy(name, "");
+    DriverInfo() {
+        strcpy(driver_name, "");
+        driver_flags = 0;
+        backend = NULL;
     }
 
     //! Initialize.
-    DriverInfo(const char* driver_name,
-               DriverType driver_type,
-               unsigned int driver_flags,
-               IBackend* driver_backend)
-        : type(driver_type)
-        , flags(driver_flags)
-        , backend(driver_backend) {
-        if (!driver_name || strlen(driver_name) > sizeof(name) - 1) {
+    DriverInfo(const char* p_driver_name,
+               unsigned int p_driver_flags,
+               IBackend* p_backend) {
+        if (!p_driver_name || strlen(p_driver_name) > sizeof(driver_name) - 1) {
             roc_panic("invalid driver name");
         }
-        strcpy(name, driver_name);
+        strcpy(driver_name, p_driver_name);
+        driver_flags = p_driver_flags;
+        backend = p_backend;
     }
 };
 
-//! Convert driver type to string.
-const char* driver_type_to_str(DriverType type);
+//! Information about format supported by "file" driver.
+struct FormatInfo {
+    //! Driver name.
+    char driver_name[12];
+
+    //! Driver flags.
+    unsigned int driver_flags;
+
+    //! Format name.
+    char format_name[12];
+
+    //! Associated backend.
+    IBackend* backend;
+
+    //! Initialize.
+    FormatInfo() {
+        strcpy(driver_name, "");
+        driver_flags = 0;
+        strcpy(format_name, "");
+        backend = NULL;
+    }
+
+    //! Initialize.
+    FormatInfo(const char* p_driver_name,
+               const char* p_format_name,
+               unsigned int p_driver_flags,
+               IBackend* p_backend) {
+        if (!p_format_name || strlen(p_format_name) > sizeof(format_name) - 1) {
+            roc_panic("invalid format name");
+        }
+        strcpy(driver_name, p_driver_name);
+        driver_flags = p_driver_flags;
+        strcpy(format_name, p_format_name);
+        backend = p_backend;
+    }
+};
 
 } // namespace sndio
 } // namespace roc
diff --git a/src/internal_modules/roc_sndio/ibackend.h b/src/internal_modules/roc_sndio/ibackend.h
index 94244f872..9abb1af6d 100644
--- a/src/internal_modules/roc_sndio/ibackend.h
+++ b/src/internal_modules/roc_sndio/ibackend.h
@@ -16,6 +16,7 @@
 #include "roc_core/array.h"
 #include "roc_core/attributes.h"
 #include "roc_core/iarena.h"
+#include "roc_core/string_list.h"
 #include "roc_sndio/device_type.h"
 #include "roc_sndio/driver.h"
 #include "roc_sndio/idevice.h"
@@ -37,12 +38,24 @@ class IBackend {
     virtual const char* name() const = 0;
 
     //! Append supported drivers to the list.
-    virtual void discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list) = 0;
+    virtual ROC_ATTR_NODISCARD bool
+    discover_drivers(core::Array<DriverInfo, MaxDrivers>& result) = 0;
+
+    //! Append supported formats to the list.
+    virtual ROC_ATTR_NODISCARD bool
+    discover_formats(core::Array<FormatInfo, MaxFormats>& result) = 0;
+
+    //! Append supported groups of sub-formats to the list.
+    virtual ROC_ATTR_NODISCARD bool
+    discover_subformat_groups(core::StringList& result) = 0;
+
+    //! Append supported sub-formats of a group to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformats(const char* group,
+                                                        core::StringList& result) = 0;
 
     //! Create and open a sink or source.
     virtual ROC_ATTR_NODISCARD status::StatusCode
     open_device(DeviceType device_type,
-                DriverType driver_type,
                 const char* driver,
                 const char* path,
                 const IoConfig& io_config,
diff --git a/src/internal_modules/roc_sndio/idevice.h b/src/internal_modules/roc_sndio/idevice.h
index 98ba2e80f..5ba59b141 100644
--- a/src/internal_modules/roc_sndio/idevice.h
+++ b/src/internal_modules/roc_sndio/idevice.h
@@ -64,6 +64,10 @@ class IDevice : public core::ArenaAllocation {
     //! Frame written to or read from the device should use this specification.
     virtual audio::SampleSpec sample_spec() const = 0;
 
+    //! Get recommended frame length of the device.
+    //! Frames written to or read from the device are recommended to have this size.
+    virtual core::nanoseconds_t frame_length() const = 0;
+
     //! Check if the device supports state updates.
     //! @remarks
     //!  If true, state() returns current state, and pause() and resume()
diff --git a/src/internal_modules/roc_sndio/io_config.h b/src/internal_modules/roc_sndio/io_config.h
index ad8e75009..f4aee8a1c 100644
--- a/src/internal_modules/roc_sndio/io_config.h
+++ b/src/internal_modules/roc_sndio/io_config.h
@@ -20,12 +20,6 @@
 namespace roc {
 namespace sndio {
 
-//! Default frame length.
-//! @remarks
-//!  10ms is rather high, but works well even on cheap sound cards and CPUs.
-//!  Usually you can use much lower values.
-const core::nanoseconds_t DefaultFrameLength = 10 * core::Millisecond;
-
 //! Sink and source config.
 struct IoConfig {
     //! Sample spec
@@ -41,7 +35,7 @@ struct IoConfig {
     IoConfig()
         : sample_spec()
         , latency(0)
-        , frame_length(DefaultFrameLength) {
+        , frame_length(0) {
     }
 };
 
diff --git a/src/internal_modules/roc_sndio/io_pump.cpp b/src/internal_modules/roc_sndio/io_pump.cpp
index f697f465a..64793ae09 100644
--- a/src/internal_modules/roc_sndio/io_pump.cpp
+++ b/src/internal_modules/roc_sndio/io_pump.cpp
@@ -7,12 +7,19 @@
  */
 
 #include "roc_sndio/io_pump.h"
+#include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_status/code_to_str.h"
 
 namespace roc {
 namespace sndio {
 
+namespace {
+
+const core::nanoseconds_t DefaultFrameLength = 10 * core::Millisecond;
+
+} // namespace
+
 IoPump::IoPump(core::IPool& frame_pool,
                core::IPool& frame_buffer_pool,
                ISource& source,
@@ -26,22 +33,29 @@ IoPump::IoPump(core::IPool& frame_pool,
     , current_source_(&source)
     , sink_(sink)
     , sample_spec_(io_config.sample_spec)
-    , frame_size_(io_config.sample_spec.ns_2_bytes(io_config.frame_length))
-    , frame_duration_(io_config.sample_spec.ns_2_stream_timestamp(io_config.frame_length))
+    , frame_size_(0)
+    , frame_duration_(0)
     , mode_(mode)
     , was_active_(false)
     , stop_(0)
+    , transferred_bytes_(0)
     , init_status_(status::NoStatus) {
-    if (frame_size_ == 0 || frame_duration_ == 0) {
-        roc_log(LogError, "pump: invalid frame length %lld",
-                (long long)io_config.frame_length);
-        init_status_ = status::StatusBadConfig;
-        return;
+    if (!io_config.sample_spec.is_complete()) {
+        roc_panic("io pump: expected complete sample spec: spec=%s",
+                  audio::sample_spec_to_str(io_config.sample_spec).c_str());
     }
 
+    core::nanoseconds_t frame_len = io_config.frame_length;
+    if (frame_len == 0) {
+        frame_len = DefaultFrameLength;
+    }
+
+    frame_size_ = io_config.sample_spec.ns_2_bytes(frame_len);
+    frame_duration_ = io_config.sample_spec.ns_2_stream_timestamp(frame_len);
+
     frame_ = frame_factory_.allocate_frame(frame_size_);
     if (!frame_) {
-        roc_log(LogError, "pump: can't allocate frame");
+        roc_log(LogError, "io pump: can't allocate frame");
         init_status_ = status::StatusNoMem;
         return;
     }
@@ -54,7 +68,7 @@ status::StatusCode IoPump::init_status() const {
 }
 
 status::StatusCode IoPump::run() {
-    roc_log(LogDebug, "pump: starting main loop");
+    roc_log(LogDebug, "io pump: starting main loop");
 
     status::StatusCode code = status::NoStatus;
 
@@ -66,7 +80,10 @@ status::StatusCode IoPump::run() {
     }
 
     if (code == status::StatusFinish) {
-        code = status::StatusOK; // EOF is fine
+        // EOF is fine
+        code = status::StatusOK;
+        roc_log(LogDebug, "io pump: transferred %.3f MB",
+                (double)transferred_bytes_ / 1024 * 1024);
     }
 
     if (code == status::StatusOK) {
@@ -78,10 +95,10 @@ status::StatusCode IoPump::run() {
         code = close_code;
     }
 
-    roc_log(LogDebug, "pump: exiting main loop");
+    roc_log(LogDebug, "io pump: exiting main loop");
 
     roc_panic_if_msg(code <= status::NoStatus || code >= status::MaxStatus,
-                     "pump: invalid status code %d", code);
+                     "io pump: invalid status code %d", code);
 
     return code;
 }
@@ -95,7 +112,7 @@ status::StatusCode IoPump::next_() {
 
     // User called stop().
     if (stop_) {
-        roc_log(LogDebug, "pump: got stop request, exiting");
+        roc_log(LogDebug, "io pump: got stop request, exiting");
         return status::StatusAbort;
     }
 
@@ -105,17 +122,17 @@ status::StatusCode IoPump::next_() {
         // inactive first time, we exit.
         if (mode_ == ModeOneshot && was_active_) {
             roc_log(LogInfo,
-                    "pump: main source became inactive in oneshot mode, exiting");
+                    "io pump: main source became inactive in oneshot mode, exiting");
             return status::StatusFinish;
         }
 
         // User specified --backup, when main source becomes inactive, we
         // switch to specified backup source.
         if (backup_source_) {
-            roc_log(LogInfo, "pump: main source became inactive, switching to backup");
+            roc_log(LogInfo, "io pump: main source became inactive, switching to backup");
 
             if ((code = backup_source_->rewind()) != status::StatusOK) {
-                roc_log(LogError, "pump: can't rewind backup source: status=%s",
+                roc_log(LogError, "io pump: can't rewind backup source: status=%s",
                         status::code_to_str(code));
                 return code;
             }
@@ -128,7 +145,7 @@ status::StatusCode IoPump::next_() {
 
     // Main source became active.
     if (current_source_ != &main_source_ && main_source_.state() == DeviceState_Active) {
-        roc_log(LogInfo, "pump: main source became active, switching to it");
+        roc_log(LogInfo, "io pump: main source became active, switching to it");
 
         if ((code = switch_source_(&main_source_)) != status::StatusOK) {
             return code;
@@ -141,23 +158,23 @@ status::StatusCode IoPump::next_() {
     if (code == status::StatusFinish) {
         // EOF from main source causes exit.
         if (current_source_ == &main_source_) {
-            roc_log(LogInfo, "pump: got eof from main source, exiting");
+            roc_log(LogInfo, "io pump: got eof from main source, exiting");
             return code;
         }
 
         // EOF from backup source causes rewind.
         if (current_source_ == backup_source_) {
-            roc_log(LogDebug, "pump: got eof from backup source, rewinding");
+            roc_log(LogDebug, "io pump: got eof from backup source, rewinding");
 
             if ((code = backup_source_->rewind()) != status::StatusOK) {
-                roc_log(LogError, "pump: can't rewind backup source: status=%s",
+                roc_log(LogError, "io pump: can't rewind backup source: status=%s",
                         status::code_to_str(code));
                 return code;
             }
         }
     } else if (code != status::StatusOK) {
         // Source or sink failure.
-        roc_log(LogError, "pump: got error when copying frame: status=%s",
+        roc_log(LogError, "io pump: got error when copying frame: status=%s",
                 status::code_to_str(code));
         return code;
     }
@@ -176,12 +193,12 @@ status::StatusCode IoPump::switch_source_(ISource* new_source) {
 
     // Switch from backup to main.
     if (new_source == &main_source_ && current_source_ != &main_source_) {
-        roc_log(LogInfo, "pump: switching to main source");
+        roc_log(LogInfo, "io pump: switching to main source");
 
         // Pause backup.
         if (backup_source_ && backup_source_->has_state()) {
             if ((code = backup_source_->pause()) != status::StatusOK) {
-                roc_log(LogError, "pump: can't pause backup source: status=%s",
+                roc_log(LogError, "io pump: can't pause backup source: status=%s",
                         status::code_to_str(code));
                 return code;
             }
@@ -189,7 +206,7 @@ status::StatusCode IoPump::switch_source_(ISource* new_source) {
 
         // Resume main.
         if ((code = main_source_.resume()) != status::StatusOK) {
-            roc_log(LogError, "pump: can't resume main source: status=%s",
+            roc_log(LogError, "io pump: can't resume main source: status=%s",
                     status::code_to_str(code));
             return code;
         }
@@ -199,13 +216,13 @@ status::StatusCode IoPump::switch_source_(ISource* new_source) {
 
     // Switch from main to backup.
     if (new_source == backup_source_ && current_source_ != backup_source_) {
-        roc_log(LogInfo, "pump: switching to backup source");
+        roc_log(LogInfo, "io pump: switching to backup source");
 
         roc_panic_if(!backup_source_);
 
         // Pause main.
         if ((code = main_source_.pause()) != status::StatusOK) {
-            roc_log(LogError, "pump: can't pause main source: status=%s",
+            roc_log(LogError, "io pump: can't pause main source: status=%s",
                     status::code_to_str(code));
             return code;
         }
@@ -213,7 +230,7 @@ status::StatusCode IoPump::switch_source_(ISource* new_source) {
         // Resume backup.
         if (backup_source_->has_state()) {
             if ((code = backup_source_->resume()) != status::StatusOK) {
-                roc_log(LogError, "pump: can't resume backup source: status=%s",
+                roc_log(LogError, "io pump: can't resume backup source: status=%s",
                         status::code_to_str(code));
                 return code;
             }
@@ -279,6 +296,8 @@ status::StatusCode IoPump::transfer_frame_(ISource& source, ISink& sink) {
         source.reclock(core::timestamp(core::ClockUnix) + playback_latency);
     }
 
+    transferred_bytes_ += frame_->num_bytes();
+
     return status::StatusOK;
 }
 
@@ -286,7 +305,7 @@ status::StatusCode IoPump::flush_sink_() {
     const status::StatusCode code = sink_.flush();
 
     if (code != status::StatusOK) {
-        roc_log(LogError, "pump: got error when flushing sink: status=%s",
+        roc_log(LogError, "io pump: got error when flushing sink: status=%s",
                 status::code_to_str(code));
     }
 
@@ -301,7 +320,7 @@ status::StatusCode IoPump::close_all_devices_() {
         if (devices[i]) {
             status::StatusCode device_code = devices[i]->close();
             if (device_code != status::StatusOK) {
-                roc_log(LogError, "pump: failed to close device: status=%s",
+                roc_log(LogError, "io pump: failed to close device: status=%s",
                         status::code_to_str(device_code));
                 if (first_error == status::StatusOK) {
                     first_error = device_code;
diff --git a/src/internal_modules/roc_sndio/io_pump.h b/src/internal_modules/roc_sndio/io_pump.h
index 9c586b093..6724e2e1a 100644
--- a/src/internal_modules/roc_sndio/io_pump.h
+++ b/src/internal_modules/roc_sndio/io_pump.h
@@ -83,13 +83,15 @@ class IoPump : public core::NonCopyable<> {
     const audio::SampleSpec sample_spec_;
 
     audio::FramePtr frame_;
-    const size_t frame_size_;
-    const packet::stream_timestamp_t frame_duration_;
+    size_t frame_size_;
+    packet::stream_timestamp_t frame_duration_;
 
     const Mode mode_;
     bool was_active_;
     core::Atomic<int> stop_;
 
+    uint64_t transferred_bytes_;
+
     status::StatusCode init_status_;
 };
 
diff --git a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp
index 7bb528c78..4320aeb9c 100644
--- a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp
+++ b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.cpp
@@ -24,34 +24,48 @@ const char* PulseaudioBackend::name() const {
     return "pulseaudio";
 }
 
-void PulseaudioBackend::discover_drivers(
-    core::Array<DriverInfo, MaxDrivers>& driver_list) {
-    if (!driver_list.push_back(DriverInfo("pulse", DriverType_Device,
-                                          DriverFlag_IsDefault | DriverFlag_SupportsSink
-                                              | DriverFlag_SupportsSource,
-                                          this))) {
-        roc_panic("pulseaudio backend: can't add driver");
+bool PulseaudioBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& result) {
+    if (!result.push_back(DriverInfo("pulse",
+                                     Driver_Device | Driver_DefaultDevice
+                                         | Driver_SupportsSink | Driver_SupportsSource,
+                                     this))) {
+        return false;
     }
+    return true;
+}
+
+bool PulseaudioBackend::discover_formats(core::Array<FormatInfo, MaxFormats>& result) {
+    // no formats except pcm
+    return true;
+}
+
+bool PulseaudioBackend::discover_subformat_groups(core::StringList& result) {
+    // no sub-formats except pcm
+    return true;
+}
+
+bool PulseaudioBackend::discover_subformats(const char* group, core::StringList& result) {
+    // no sub-formats except pcm
+    return true;
 }
 
 status::StatusCode PulseaudioBackend::open_device(DeviceType device_type,
-                                                  DriverType driver_type,
                                                   const char* driver,
                                                   const char* path,
                                                   const IoConfig& io_config,
                                                   audio::FrameFactory& frame_factory,
                                                   core::IArena& arena,
                                                   IDevice** result) {
-    if (driver_type != DriverType_Device) {
-        return status::StatusNoDriver;
-    }
+    roc_panic_if(!driver);
+    roc_panic_if(!path);
 
-    if (driver && strcmp(driver, "pulse") != 0) {
+    if (strcmp(driver, "pulse") != 0) {
+        // Not pulse://, go to next backend.
         return status::StatusNoDriver;
     }
 
     core::ScopedPtr<PulseaudioDevice> device(
-        new (arena) PulseaudioDevice(frame_factory, arena, io_config, device_type));
+        new (arena) PulseaudioDevice(frame_factory, arena, io_config, device_type, path));
 
     if (!device) {
         roc_log(LogDebug, "pulseaudio backend: can't allocate device: path=%s", path);
@@ -59,17 +73,9 @@ status::StatusCode PulseaudioBackend::open_device(DeviceType device_type,
     }
 
     if (device->init_status() != status::StatusOK) {
-        roc_log(LogDebug,
-                "pulseaudio backend: can't initialize device: path=%s status=%s", path,
-                status::code_to_str(device->init_status()));
-        return device->init_status();
-    }
-
-    const status::StatusCode code = device->open(path);
-    if (code != status::StatusOK) {
         roc_log(LogDebug, "pulseaudio backend: can't open device: path=%s status=%s",
-                path, status::code_to_str(code));
-        return code;
+                path, status::code_to_str(device->init_status()));
+        return device->init_status();
     }
 
     *result = device.hijack();
diff --git a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h
index d163aa2ce..da3aa69cb 100644
--- a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h
+++ b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_backend.h
@@ -27,12 +27,23 @@ class PulseaudioBackend : public IBackend, core::NonCopyable<> {
     virtual const char* name() const;
 
     //! Append supported drivers to the list.
-    virtual void discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list);
+    virtual ROC_ATTR_NODISCARD bool
+    discover_drivers(core::Array<DriverInfo, MaxDrivers>& result);
+
+    //! Append supported formats to the list.
+    virtual ROC_ATTR_NODISCARD bool
+    discover_formats(core::Array<FormatInfo, MaxFormats>& result);
+
+    //! Append supported groups of sub-formats to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformat_groups(core::StringList& result);
+
+    //! Append supported sub-formats of a group to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformats(const char* group,
+                                                        core::StringList& result);
 
     //! Create and open a sink or source.
     virtual ROC_ATTR_NODISCARD status::StatusCode
     open_device(DeviceType device_type,
-                DriverType driver_type,
                 const char* driver,
                 const char* path,
                 const IoConfig& io_config,
diff --git a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.cpp b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.cpp
index 5cb1e9367..1f252ed21 100644
--- a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.cpp
+++ b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.cpp
@@ -8,8 +8,8 @@
 
 #include "roc_sndio/pulseaudio_device.h"
 #include "roc_audio/channel_defs.h"
+#include "roc_audio/format.h"
 #include "roc_audio/sample.h"
-#include "roc_audio/sample_format.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
@@ -27,86 +27,90 @@ const core::nanoseconds_t ReportInterval = 10 * core::Second;
 // 40ms or 20ms, and sometimes even 10ms
 const core::nanoseconds_t DefaultLatency = core::Millisecond * 60;
 
+// 10ms is rather high, but works well even on cheap sound cards and CPUs.
+// Usually you can use much lower values.
+const core::nanoseconds_t DefaultFrameLength = 10 * core::Millisecond;
+
 const core::nanoseconds_t MinTimeout = core::Millisecond * 50;
 const core::nanoseconds_t MaxTimeout = core::Second * 2;
 
-audio::PcmFormat from_pulse_format(pa_sample_format fmt) {
+audio::PcmSubformat from_pulse_format(pa_sample_format fmt) {
     switch (fmt) {
     case PA_SAMPLE_U8:
-        return audio::PcmFormat_UInt8;
+        return audio::PcmSubformat_UInt8;
 
     case PA_SAMPLE_S16LE:
-        return audio::PcmFormat_SInt16_Le;
+        return audio::PcmSubformat_SInt16_Le;
     case PA_SAMPLE_S16BE:
-        return audio::PcmFormat_SInt16_Be;
+        return audio::PcmSubformat_SInt16_Be;
 
     case PA_SAMPLE_S24LE:
-        return audio::PcmFormat_SInt24_Le;
+        return audio::PcmSubformat_SInt24_Le;
     case PA_SAMPLE_S24BE:
-        return audio::PcmFormat_SInt24_Be;
+        return audio::PcmSubformat_SInt24_Be;
 
     case PA_SAMPLE_S24_32LE:
-        return audio::PcmFormat_SInt24_4_Le;
+        return audio::PcmSubformat_SInt24_4_Le;
     case PA_SAMPLE_S24_32BE:
-        return audio::PcmFormat_SInt24_4_Be;
+        return audio::PcmSubformat_SInt24_4_Be;
 
     case PA_SAMPLE_S32LE:
-        return audio::PcmFormat_SInt32_Le;
+        return audio::PcmSubformat_SInt32_Le;
     case PA_SAMPLE_S32BE:
-        return audio::PcmFormat_SInt32_Be;
+        return audio::PcmSubformat_SInt32_Be;
 
     case PA_SAMPLE_FLOAT32LE:
-        return audio::PcmFormat_Float32_Le;
+        return audio::PcmSubformat_Float32_Le;
     case PA_SAMPLE_FLOAT32BE:
-        return audio::PcmFormat_Float32_Be;
+        return audio::PcmSubformat_Float32_Be;
 
     default:
         break;
     }
 
-    return audio::PcmFormat_Invalid;
+    return audio::PcmSubformat_Invalid;
 }
 
-pa_sample_format to_pulse_format(audio::PcmFormat fmt) {
+pa_sample_format to_pulse_format(audio::PcmSubformat fmt) {
     switch (fmt) {
-    case audio::PcmFormat_UInt8:
-    case audio::PcmFormat_UInt8_Le:
-    case audio::PcmFormat_UInt8_Be:
+    case audio::PcmSubformat_UInt8:
+    case audio::PcmSubformat_UInt8_Le:
+    case audio::PcmSubformat_UInt8_Be:
         return PA_SAMPLE_U8;
 
-    case audio::PcmFormat_SInt16:
+    case audio::PcmSubformat_SInt16:
         return PA_SAMPLE_S16NE;
-    case audio::PcmFormat_SInt16_Le:
+    case audio::PcmSubformat_SInt16_Le:
         return PA_SAMPLE_S16LE;
-    case audio::PcmFormat_SInt16_Be:
+    case audio::PcmSubformat_SInt16_Be:
         return PA_SAMPLE_S16BE;
 
-    case audio::PcmFormat_SInt24:
+    case audio::PcmSubformat_SInt24:
         return PA_SAMPLE_S24NE;
-    case audio::PcmFormat_SInt24_Le:
+    case audio::PcmSubformat_SInt24_Le:
         return PA_SAMPLE_S24LE;
-    case audio::PcmFormat_SInt24_Be:
+    case audio::PcmSubformat_SInt24_Be:
         return PA_SAMPLE_S24BE;
 
-    case audio::PcmFormat_SInt24_4:
+    case audio::PcmSubformat_SInt24_4:
         return PA_SAMPLE_S24_32NE;
-    case audio::PcmFormat_SInt24_4_Le:
+    case audio::PcmSubformat_SInt24_4_Le:
         return PA_SAMPLE_S24_32LE;
-    case audio::PcmFormat_SInt24_4_Be:
+    case audio::PcmSubformat_SInt24_4_Be:
         return PA_SAMPLE_S24_32BE;
 
-    case audio::PcmFormat_SInt32:
+    case audio::PcmSubformat_SInt32:
         return PA_SAMPLE_S32NE;
-    case audio::PcmFormat_SInt32_Le:
+    case audio::PcmSubformat_SInt32_Le:
         return PA_SAMPLE_S32LE;
-    case audio::PcmFormat_SInt32_Be:
+    case audio::PcmSubformat_SInt32_Be:
         return PA_SAMPLE_S32BE;
 
-    case audio::PcmFormat_Float32:
+    case audio::PcmSubformat_Float32:
         return PA_SAMPLE_FLOAT32NE;
-    case audio::PcmFormat_Float32_Le:
+    case audio::PcmSubformat_Float32_Le:
         return PA_SAMPLE_FLOAT32LE;
-    case audio::PcmFormat_Float32_Be:
+    case audio::PcmSubformat_Float32_Be:
         return PA_SAMPLE_FLOAT32BE;
 
     default:
@@ -121,7 +125,8 @@ pa_sample_format to_pulse_format(audio::PcmFormat fmt) {
 PulseaudioDevice::PulseaudioDevice(audio::FrameFactory& frame_factory,
                                    core::IArena& arena,
                                    const IoConfig& io_config,
-                                   DeviceType device_type)
+                                   DeviceType device_type,
+                                   const char* device)
     : IDevice(arena)
     , ISink(arena)
     , ISource(arena)
@@ -148,14 +153,24 @@ PulseaudioDevice::PulseaudioDevice(audio::FrameFactory& frame_factory,
     , timer_deadline_ns_(0)
     , rate_limiter_(ReportInterval)
     , init_status_(status::NoStatus) {
-    if (sample_spec_.sample_format() != audio::SampleFormat_Invalid
-        && (sample_spec_.sample_format() != audio::SampleFormat_Pcm
-            || to_pulse_format(sample_spec_.pcm_format()) == PA_SAMPLE_INVALID)) {
+    if (io_config.sample_spec.has_format()
+        && io_config.sample_spec.format() != audio::Format_Pcm) {
         roc_log(LogError,
-                "pulseaudio %s: requested sample format not supported by backend:"
-                " sample_spec=%s",
-                device_type_to_str(device_type_),
-                audio::sample_spec_to_str(sample_spec_).c_str());
+                "pulseaudio %s: invalid io encoding:"
+                " <format> '%s' not supported by backend: spec=%s",
+                device_type_to_str(device_type_), io_config.sample_spec.format_name(),
+                audio::sample_spec_to_str(io_config.sample_spec).c_str());
+        init_status_ = status::StatusBadConfig;
+        return;
+    }
+
+    if (io_config.sample_spec.has_subformat()
+        && to_pulse_format(io_config.sample_spec.pcm_subformat()) == PA_SAMPLE_INVALID) {
+        roc_log(LogError,
+                "pulseaudio %s: invalid io encoding:"
+                " <subformat> '%s' not supported by backend: spec=%s",
+                device_type_to_str(device_type_), io_config.sample_spec.format_name(),
+                audio::sample_spec_to_str(io_config.sample_spec).c_str());
         init_status_ = status::StatusBadConfig;
         return;
     }
@@ -176,6 +191,21 @@ PulseaudioDevice::PulseaudioDevice(audio::FrameFactory& frame_factory,
         timeout_ns_ = MaxTimeout;
     }
 
+    roc_log(LogDebug, "pulseaudio %s: opening device: device=%s",
+            device_type_to_str(device_type_), device);
+
+    if (device && strcmp(device, "default") != 0) {
+        device_ = device;
+    }
+
+    if ((init_status_ = start_mainloop_()) != status::StatusOK) {
+        return;
+    }
+
+    if ((init_status_ = open_()) != status::StatusOK) {
+        return;
+    }
+
     init_status_ = status::StatusOK;
 }
 
@@ -191,32 +221,6 @@ status::StatusCode PulseaudioDevice::init_status() const {
     return init_status_;
 }
 
-status::StatusCode PulseaudioDevice::open(const char* device) {
-    if (mainloop_) {
-        roc_panic("pulseaudio %s: can't call open() twice",
-                  device_type_to_str(device_type_));
-    }
-
-    roc_log(LogDebug, "pulseaudio %s: opening device: device=%s",
-            device_type_to_str(device_type_), device);
-
-    if (device && strcmp(device, "default") != 0) {
-        device_ = device;
-    }
-
-    status::StatusCode code = status::NoStatus;
-
-    if ((code = start_mainloop_()) != status::StatusOK) {
-        return code;
-    }
-
-    if ((code = open_()) != status::StatusOK) {
-        return code;
-    }
-
-    return status::StatusOK;
-}
-
 DeviceType PulseaudioDevice::type() const {
     return device_type_;
 }
@@ -241,6 +245,18 @@ audio::SampleSpec PulseaudioDevice::sample_spec() const {
     return sample_spec;
 }
 
+core::nanoseconds_t PulseaudioDevice::frame_length() const {
+    want_mainloop_();
+
+    pa_threaded_mainloop_lock(mainloop_);
+
+    const core::nanoseconds_t frame_len = frame_len_ns_;
+
+    pa_threaded_mainloop_unlock(mainloop_);
+
+    return frame_len;
+}
+
 bool PulseaudioDevice::has_state() const {
     return true;
 }
@@ -648,17 +664,17 @@ void PulseaudioDevice::device_info_cb_(pa_context*,
 }
 
 bool PulseaudioDevice::load_device_params_(const pa_sample_spec& device_spec) {
-    if (sample_spec_.sample_format() == audio::SampleFormat_Invalid) {
-        audio::PcmFormat fmt = from_pulse_format(device_spec.format);
+    if (sample_spec_.format() == audio::Format_Invalid) {
+        audio::PcmSubformat fmt = from_pulse_format(device_spec.format);
 
-        if (fmt == audio::PcmFormat_Invalid) {
-            // We don't support device's native format, so ask pulseaudio
-            // to do conversion for our native format.
-            fmt = audio::Sample_RawFormat;
+        if (fmt == audio::PcmSubformat_Invalid) {
+            // If don't support device's native format, ask pulseaudio
+            // to do conversion to our native format.
+            fmt = audio::PcmSubformat_Raw;
         }
 
-        sample_spec_.set_sample_format(audio::SampleFormat_Pcm);
-        sample_spec_.set_pcm_format(fmt);
+        sample_spec_.set_format(audio::Format_Pcm);
+        sample_spec_.set_pcm_subformat(fmt);
     }
 
     if (sample_spec_.sample_rate() == 0) {
@@ -671,7 +687,7 @@ bool PulseaudioDevice::load_device_params_(const pa_sample_spec& device_spec) {
         sample_spec_.channel_set().set_count(device_spec.channels);
     }
 
-    if (!sample_spec_.is_valid()) {
+    if (!sample_spec_.is_complete()) {
         roc_log(LogError,
                 "pulseaudio %s: can't determine device sample spec:"
                 " sample_spec=%s",
@@ -717,7 +733,7 @@ bool PulseaudioDevice::load_device_params_(const pa_sample_spec& device_spec) {
 }
 
 bool PulseaudioDevice::init_stream_params_(const pa_sample_spec& device_spec) {
-    stream_spec_.format = to_pulse_format(sample_spec_.pcm_format());
+    stream_spec_.format = to_pulse_format(sample_spec_.pcm_subformat());
     stream_spec_.rate = (uint32_t)sample_spec_.sample_rate();
     stream_spec_.channels = (uint8_t)sample_spec_.num_channels();
 
diff --git a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.h b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.h
index c3f352c6f..29e0b0a94 100644
--- a/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.h
+++ b/src/internal_modules/roc_sndio/target_pulseaudio/roc_sndio/pulseaudio_device.h
@@ -37,15 +37,13 @@ class PulseaudioDevice : public ISink, public ISource, public core::NonCopyable<
     PulseaudioDevice(audio::FrameFactory& frame_factory,
                      core::IArena& arena,
                      const IoConfig& io_config,
-                     DeviceType device_type);
+                     DeviceType device_type,
+                     const char* device);
     ~PulseaudioDevice();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open device.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* device);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -58,6 +56,9 @@ class PulseaudioDevice : public ISink, public ISource, public core::NonCopyable<
     //! Get sample specification of the device.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the device.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the device supports state updates.
     virtual bool has_state() const;
 
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp
index 223c0e262..118f51fc1 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.cpp
@@ -25,116 +25,145 @@ const char* SndfileBackend::name() const {
     return "sndfile";
 }
 
-void SndfileBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list) {
+bool SndfileBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& result) {
+    if (!result.push_back(DriverInfo(
+            "file", Driver_File | Driver_SupportsSink | Driver_SupportsSource, this))) {
+        return false;
+    }
+    return true;
+}
+
+bool SndfileBackend::discover_formats(core::Array<FormatInfo, MaxFormats>& result) {
     SF_FORMAT_INFO format_info;
-    int total_number_of_drivers = 0;
+    int major_format_count = 0;
 
-    if (int err = sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &total_number_of_drivers,
+    if (int err = sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_format_count,
                              sizeof(int))) {
-        roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed: %s",
-                  sf_error_number(err));
+        roc_log(LogError,
+                "sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed: %s",
+                sf_error_number(err));
+        return false;
     }
 
-    for (int format_index = 0; format_index < total_number_of_drivers; format_index++) {
-        format_info.format = format_index;
+    for (int fmt_index = 0; fmt_index < major_format_count; fmt_index++) {
+        format_info.format = fmt_index;
         if (int err = sf_command(NULL, SFC_GET_FORMAT_MAJOR, &format_info,
                                  sizeof(format_info))) {
-            roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed: %s",
-                      sf_error_number(err));
+            roc_log(LogError,
+                    "sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed: %s",
+                    sf_error_number(err));
+            return false;
         }
 
-        const char* driver = format_info.extension;
+        // Format name = file extension.
+        const char* format_name = format_info.extension;
 
-        for (size_t map_index = 0; map_index < ROC_ARRAY_SIZE(sndfile_driver_remap);
+        for (size_t map_index = 0; map_index < ROC_ARRAY_SIZE(sndfile_format_remap);
              map_index++) {
-            if ((sndfile_driver_remap[map_index].format_mask & SF_FORMAT_TYPEMASK)
+            if ((sndfile_format_remap[map_index].format_mask & SF_FORMAT_TYPEMASK)
                 == format_info.format) {
-                driver = sndfile_driver_remap[map_index].driver_name;
+                // Some format names are remapped.
+                format_name = sndfile_format_remap[map_index].name;
             }
         }
 
-        if (!driver_list.push_back(
-                DriverInfo(driver, DriverType_File,
-                           DriverFlag_SupportsSource | DriverFlag_SupportsSink, this))) {
-            roc_panic("sndfile backend: allocation failed");
+        if (!result.push_back(FormatInfo(
+                "file", format_name,
+                Driver_File | Driver_SupportsSource | Driver_SupportsSink, this))) {
+            roc_log(LogError, "sndfile backend: allocation failed");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool SndfileBackend::discover_subformat_groups(core::StringList& result) {
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(sndfile_subformat_map); n++) {
+        if (result.find(sndfile_subformat_map[n].group)) {
+            continue;
+        }
+        if (!result.push_back(sndfile_subformat_map[n].group)) {
+            roc_log(LogError, "sndfile backend: allocation failed");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool SndfileBackend::discover_subformats(const char* group, core::StringList& result) {
+    roc_panic_if(!group);
+
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(sndfile_subformat_map); n++) {
+        if (strcmp(sndfile_subformat_map[n].group, group) != 0) {
+            continue;
+        }
+        if (result.find(sndfile_subformat_map[n].name)) {
+            continue;
+        }
+        if (!result.push_back(sndfile_subformat_map[n].name)) {
+            roc_log(LogError, "sndfile backend: allocation failed");
+            return false;
         }
     }
+
+    return true;
 }
 
 status::StatusCode SndfileBackend::open_device(DeviceType device_type,
-                                               DriverType driver_type,
                                                const char* driver,
                                                const char* path,
                                                const IoConfig& io_config,
                                                audio::FrameFactory& frame_factory,
                                                core::IArena& arena,
                                                IDevice** result) {
-    if (driver_type != DriverType_File) {
+    roc_panic_if(!driver);
+    roc_panic_if(!path);
+
+    if (strcmp(driver, "file") != 0) {
+        // Not file://, go to next backend.
         return status::StatusNoDriver;
     }
 
     switch (device_type) {
     case DeviceType_Sink: {
         core::ScopedPtr<SndfileSink> sink(
-            new (arena) SndfileSink(frame_factory, arena, io_config));
+            new (arena) SndfileSink(frame_factory, arena, io_config, path));
 
         if (!sink) {
-            roc_log(LogDebug, "sndfile backend: can't allocate sink: driver=%s path=%s",
-                    driver, path);
+            roc_log(LogDebug, "sndfile backend: can't allocate sink: path=%s", path);
             return status::StatusNoMem;
         }
 
         if (sink->init_status() != status::StatusOK) {
-            roc_log(LogDebug,
-                    "sndfile backend: can't initialize sink: driver=%s path=%s status=%s",
-                    driver, path, status::code_to_str(sink->init_status()));
+            roc_log(LogDebug, "sndfile backend: can't open sink: path=%s status=%s", path,
+                    status::code_to_str(sink->init_status()));
             return sink->init_status();
         }
 
-        const status::StatusCode code = sink->open(driver, path);
-        if (code != status::StatusOK) {
-            roc_log(LogDebug,
-                    "sndfile backend: can't open sink: driver=%s path=%s status=%s",
-                    driver, path, status::code_to_str(code));
-            return code;
-        }
-
         *result = sink.hijack();
         return status::StatusOK;
     } break;
 
     case DeviceType_Source: {
         core::ScopedPtr<SndfileSource> source(
-            new (arena) SndfileSource(frame_factory, arena, io_config));
+            new (arena) SndfileSource(frame_factory, arena, io_config, path));
 
         if (!source) {
-            roc_log(LogDebug, "sndfile backend: can't allocate source: driver=%s path=%s",
-                    driver, path);
+            roc_log(LogDebug, "sndfile backend: can't allocate source: path=%s", path);
             return status::StatusNoMem;
         }
 
         if (source->init_status() != status::StatusOK) {
-            roc_log(
-                LogDebug,
-                "sndfile backend: can't initialize source: driver=%s path=%s status=%s",
-                driver, path, status::code_to_str(source->init_status()));
+            roc_log(LogDebug, "sndfile backend: can't open source: path=%s status=%s",
+                    path, status::code_to_str(source->init_status()));
             return source->init_status();
         }
 
-        const status::StatusCode code = source->open(driver, path);
-        if (code != status::StatusOK) {
-            roc_log(LogDebug,
-                    "sndfile backend: can't open source: driver=%s path=%s status=%s",
-                    driver, path, status::code_to_str(code));
-            return code;
-        }
-
         *result = source.hijack();
         return status::StatusOK;
     } break;
-
-    default:
-        break;
     }
 
     roc_panic("sndfile backend: invalid device type");
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h
index aca7a3198..268193a29 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_backend.h
@@ -27,12 +27,23 @@ class SndfileBackend : public IBackend, core::NonCopyable<> {
     virtual const char* name() const;
 
     //! Append supported drivers to the list.
-    virtual void discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list);
+    virtual ROC_ATTR_NODISCARD bool
+    discover_drivers(core::Array<DriverInfo, MaxDrivers>& result);
+
+    //! Append supported formats to the list.
+    virtual ROC_ATTR_NODISCARD bool
+    discover_formats(core::Array<FormatInfo, MaxFormats>& result);
+
+    //! Append supported groups of sub-formats to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformat_groups(core::StringList& result);
+
+    //! Append supported sub-formats of a group to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformats(const char* group,
+                                                        core::StringList& result);
 
     //! Create and open a sink or source.
     virtual ROC_ATTR_NODISCARD status::StatusCode
     open_device(DeviceType device_type,
-                DriverType driver_type,
                 const char* driver,
                 const char* path,
                 const IoConfig& io_config,
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.cpp
new file mode 100644
index 000000000..12b093470
--- /dev/null
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.cpp
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2024 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include "roc_sndio/sndfile_helpers.h"
+#include "roc_sndio/sndfile_tables.h"
+
+namespace roc {
+namespace sndio {
+
+namespace {
+
+int pcm_2_sf(audio::PcmSubformat fmt) {
+    switch (fmt) {
+    case audio::PcmSubformat_UInt8:
+    case audio::PcmSubformat_UInt8_Le:
+    case audio::PcmSubformat_UInt8_Be:
+        return SF_FORMAT_PCM_U8 | SF_ENDIAN_FILE;
+
+    case audio::PcmSubformat_SInt8:
+    case audio::PcmSubformat_SInt8_Le:
+    case audio::PcmSubformat_SInt8_Be:
+        return SF_FORMAT_PCM_S8 | SF_ENDIAN_FILE;
+
+    case audio::PcmSubformat_SInt16:
+        return SF_FORMAT_PCM_16 | SF_ENDIAN_FILE;
+    case audio::PcmSubformat_SInt16_Le:
+        return SF_FORMAT_PCM_16 | SF_ENDIAN_LITTLE;
+    case audio::PcmSubformat_SInt16_Be:
+        return SF_FORMAT_PCM_16 | SF_ENDIAN_BIG;
+
+    case audio::PcmSubformat_SInt24:
+        return SF_FORMAT_PCM_24 | SF_ENDIAN_FILE;
+    case audio::PcmSubformat_SInt24_Le:
+        return SF_FORMAT_PCM_24 | SF_ENDIAN_LITTLE;
+    case audio::PcmSubformat_SInt24_Be:
+        return SF_FORMAT_PCM_24 | SF_ENDIAN_BIG;
+
+    case audio::PcmSubformat_SInt32:
+        return SF_FORMAT_PCM_32 | SF_ENDIAN_FILE;
+    case audio::PcmSubformat_SInt32_Le:
+        return SF_FORMAT_PCM_32 | SF_ENDIAN_LITTLE;
+    case audio::PcmSubformat_SInt32_Be:
+        return SF_FORMAT_PCM_32 | SF_ENDIAN_BIG;
+
+    case audio::PcmSubformat_Float32:
+        return SF_FORMAT_FLOAT | SF_ENDIAN_FILE;
+    case audio::PcmSubformat_Float32_Le:
+        return SF_FORMAT_FLOAT | SF_ENDIAN_LITTLE;
+    case audio::PcmSubformat_Float32_Be:
+        return SF_FORMAT_FLOAT | SF_ENDIAN_BIG;
+
+    case audio::PcmSubformat_Float64:
+        return SF_FORMAT_DOUBLE | SF_ENDIAN_FILE;
+    case audio::PcmSubformat_Float64_Le:
+        return SF_FORMAT_DOUBLE | SF_ENDIAN_LITTLE;
+    case audio::PcmSubformat_Float64_Be:
+        return SF_FORMAT_DOUBLE | SF_ENDIAN_BIG;
+
+    default:
+        break;
+    }
+
+    return 0;
+}
+
+} // namespace
+
+status::StatusCode sndfile_select_major_format(SF_INFO& file_info,
+                                               audio::SampleSpec& sample_spec,
+                                               const char* path) {
+    roc_panic_if(!path);
+
+    const char* file_extension = NULL;
+    const char* dot = strrchr(path, '.');
+    if (dot && dot != path) {
+        file_extension = dot;
+    }
+
+    // First try to select format by iterating through sndfile_driver_remap.
+    if (sample_spec.has_format()) {
+        // If format is specified, match by format name.
+        for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_format_remap); idx++) {
+            if (strcmp(sample_spec.format_name(), sndfile_format_remap[idx].name) == 0) {
+                file_info.format = sndfile_format_remap[idx].format_mask;
+                return status::StatusOK;
+            }
+        }
+    } else if (file_extension != NULL) {
+        // If format is omitted, match by file extension.
+        for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_format_remap); idx++) {
+            if (sndfile_format_remap[idx].file_extension != NULL) {
+                if (strcmp(file_extension, sndfile_format_remap[idx].file_extension)
+                    == 0) {
+                    file_info.format = sndfile_format_remap[idx].format_mask;
+                    if (!sample_spec.set_custom_format(sndfile_format_remap[idx].name)) {
+                        continue;
+                    }
+                    return status::StatusOK;
+                }
+            }
+        }
+    }
+
+    // Then try to select format by iterating through all sndfile major formats.
+    int major_format_count = 0;
+    if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_format_count,
+                                sizeof(int))) {
+        roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed: %s",
+                  sf_error_number(errnum));
+    }
+
+    for (int idx = 0; idx < major_format_count; idx++) {
+        SF_FORMAT_INFO format_info;
+        memset(&format_info, 0, sizeof(format_info));
+        format_info.format = idx;
+        if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR, &format_info,
+                                    sizeof(format_info))) {
+            roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed: %s",
+                      sf_error_number(errnum));
+        }
+
+        if (sample_spec.has_format()) {
+            // If format is specified, match by format name.
+            // Note that format name = file extension.
+            if (strcmp(format_info.extension, sample_spec.format_name()) == 0) {
+                file_info.format = format_info.format;
+                return status::StatusOK;
+            }
+        } else if (file_extension != NULL) {
+            // If format is omitted, match by file extension.
+            if (strcmp(format_info.extension, file_extension) == 0) {
+                file_info.format = format_info.format;
+                if (!sample_spec.set_custom_format(format_info.name)) {
+                    continue;
+                }
+                return status::StatusOK;
+            }
+        }
+    }
+
+    if (sample_spec.has_format()) {
+        roc_log(
+            LogDebug,
+            "sndfile backend: requested format '%s' not supported by backend: path=%s",
+            sample_spec.format_name(), path);
+    } else {
+        roc_log(LogDebug,
+                "sndfile backend: can't detect file format from extension: path=%s",
+                path);
+    }
+    // Not a wav file, go to next backend.
+    return status::StatusNoFormat;
+}
+
+status::StatusCode sndfile_select_sub_format(SF_INFO& file_info,
+                                             audio::SampleSpec& sample_spec,
+                                             const char* path) {
+    roc_panic_if(!path);
+
+    const int format_mask = file_info.format;
+
+    // If sub-format is specified, use it.
+    if (sample_spec.has_subformat()) {
+        int subformat_mask = 0;
+
+        if (sample_spec.pcm_subformat() != audio::PcmSubformat_Invalid) {
+            // PCM sub-formats are mapped by enum.
+            subformat_mask = pcm_2_sf(sample_spec.pcm_subformat());
+        } else {
+            // Other sub-formats are mapped by string name.
+            for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_subformat_map); idx++) {
+                if (strcmp(sample_spec.subformat_name(), sndfile_subformat_map[idx].name)
+                    == 0) {
+                    subformat_mask = sndfile_subformat_map[idx].format_mask;
+                    break;
+                }
+            }
+        }
+
+        if (subformat_mask != 0) {
+            file_info.format = format_mask | subformat_mask;
+
+            if (sf_format_check(&file_info)) {
+                return status::StatusOK;
+            }
+        }
+
+        roc_log(LogError,
+                "sndfile backend: invalid io encoding:"
+                " <subformat> '%s' not allowed when <format> is '%s'",
+                sample_spec.subformat_name(), sample_spec.format_name());
+        return status::StatusBadConfig;
+    }
+
+    // For some formats, sub-format must be always specified explicitly.
+    for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_explicit_formats); idx++) {
+        if (file_info.format == sndfile_explicit_formats[idx]) {
+            roc_log(LogError,
+                    "sndfile backend: invalid io encoding:"
+                    " <subformat> is required when <format> is '%s'",
+                    sample_spec.format_name());
+            return status::StatusBadConfig;
+        }
+    }
+
+    // If sub-format is omitted, first try if we can work without sub-format.
+    file_info.format = format_mask;
+
+    if (sf_format_check(&file_info)) {
+        return status::StatusOK;
+    }
+
+    // We can't work without sub-format, choose one of the default sub-formats.
+    for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_default_subformats); idx++) {
+        const int subformat_mask = sndfile_default_subformats[idx];
+
+        file_info.format = format_mask | subformat_mask;
+
+        if (sf_format_check(&file_info)) {
+            return status::StatusOK;
+        }
+    }
+
+    roc_log(LogError,
+            "sndfile backend: invalid io encoding:"
+            " <subformat> is required when <format> is '%s'",
+            sample_spec.format_name());
+    return status::StatusBadConfig;
+}
+
+status::StatusCode sndfile_check_input_spec(const SF_INFO& file_info,
+                                            const audio::SampleSpec& sample_spec,
+                                            const char* path) {
+    roc_panic_if(!path);
+
+    bool is_explicit = false;
+
+    for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_explicit_formats); idx++) {
+        if (file_info.format == sndfile_explicit_formats[idx]) {
+            is_explicit = true;
+            break;
+        }
+    }
+
+    if (is_explicit) {
+        if (!sample_spec.has_subformat() || !sample_spec.has_sample_rate()
+            || !sample_spec.has_channel_set()) {
+            roc_log(LogError,
+                    "sndfile backend: invalid io encoding: <subformat>, <rate> and"
+                    " <channels> required for input file when <format> is '%s'",
+                    sample_spec.format_name());
+            return status::StatusBadConfig;
+        }
+    } else {
+        if (sample_spec.has_subformat() || sample_spec.has_sample_rate()
+            || sample_spec.has_channel_set()) {
+            roc_log(LogError,
+                    "sndfile backend: invalid io encoding: <subformat>, <rate> and"
+                    " <channels> not allowed for input file when <format> is '%s'",
+                    sample_spec.format_name());
+            return status::StatusBadConfig;
+        }
+    }
+
+    return status::StatusOK;
+}
+
+status::StatusCode sndfile_detect_format(const SF_INFO& file_info,
+                                         audio::SampleSpec& sample_spec) {
+    if (!sample_spec.has_format()) {
+        // First check sndfile_driver_remap.
+        for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_format_remap); idx++) {
+            if ((file_info.format & sndfile_format_remap[idx].format_mask)
+                == sndfile_format_remap[idx].format_mask) {
+                if (!sample_spec.set_custom_format(sndfile_format_remap[idx].name)) {
+                    continue;
+                }
+                break;
+            }
+        }
+
+        // Then check rest major formats.
+        int major_format_count = 0;
+        if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_format_count,
+                                    sizeof(int))) {
+            roc_panic(
+                "sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed: %s",
+                sf_error_number(errnum));
+        }
+
+        for (int idx = 0; idx < major_format_count; idx++) {
+            SF_FORMAT_INFO format_info;
+            memset(&format_info, 0, sizeof(format_info));
+            format_info.format = idx;
+            if (int errnum = sf_command(NULL, SFC_GET_FORMAT_MAJOR, &format_info,
+                                        sizeof(format_info))) {
+                roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed: %s",
+                          sf_error_number(errnum));
+            }
+
+            if ((file_info.format & format_info.format) == format_info.format) {
+                if (!sample_spec.set_custom_format(format_info.extension)) {
+                    continue;
+                }
+                break;
+            }
+        }
+
+        if (!sample_spec.has_format()) {
+            roc_log(LogError, "sndfile backend: can't detect file format");
+            return status::StatusErrFile;
+        }
+    }
+
+    if (!sample_spec.has_subformat()) {
+        // First check pcm sub-formats.
+        for (int subfmt = audio::PcmSubformat_Invalid; subfmt < audio::PcmSubformat_Max;
+             subfmt++) {
+            const int subfmt_mask =
+                pcm_2_sf((audio::PcmSubformat)subfmt) & SF_FORMAT_SUBMASK;
+
+            if ((file_info.format & subfmt_mask) == subfmt_mask) {
+                sample_spec.set_pcm_subformat((audio::PcmSubformat)subfmt);
+                break;
+            }
+        }
+
+        // Then check rest sub-formats.
+        for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_subformat_map); idx++) {
+            if ((file_info.format & sndfile_subformat_map[idx].format_mask)
+                == sndfile_subformat_map[idx].format_mask) {
+                if (!sample_spec.set_custom_subformat(sndfile_subformat_map[idx].name)) {
+                    continue;
+                }
+                break;
+            }
+        }
+    }
+
+    return status::StatusOK;
+}
+
+} // namespace sndio
+} // namespace roc
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.h
new file mode 100644
index 000000000..50cc1efb5
--- /dev/null
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+//! @file roc_sndio/target_sndfile/roc_sndio/sndfile_helpers.h
+//! @brief Sndfile helpers.
+
+#ifndef ROC_SNDIO_SNDFILE_HELPERS_H_
+#define ROC_SNDIO_SNDFILE_HELPERS_H_
+
+#include "roc_audio/sample_spec.h"
+#include "roc_core/attributes.h"
+#include "roc_status/status_code.h"
+
+#include <sndfile.h>
+
+namespace roc {
+namespace sndio {
+
+//! Choose sndfile major format from sample spec and path.
+ROC_ATTR_NODISCARD status::StatusCode sndfile_select_major_format(
+    SF_INFO& file_info, audio::SampleSpec& sample_spec, const char* path);
+
+//! Choose sndfile sub-format from sample spec and path.
+ROC_ATTR_NODISCARD status::StatusCode sndfile_select_sub_format(
+    SF_INFO& file_info, audio::SampleSpec& sample_spec, const char* path);
+
+//! Check that requested specification is valid for given input file.
+ROC_ATTR_NODISCARD status::StatusCode sndfile_check_input_spec(
+    const SF_INFO& file_info, const audio::SampleSpec& sample_spec, const char* path);
+
+//! Detect format and sub-format of opened file and fill sample spec.
+ROC_ATTR_NODISCARD status::StatusCode
+sndfile_detect_format(const SF_INFO& file_info, audio::SampleSpec& sample_spec);
+
+} // namespace sndio
+} // namespace roc
+
+#endif // ROC_SNDIO_SNDFILE_HELPERS_H_
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp
index edf0ab868..65c90d6a3 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.cpp
@@ -11,252 +11,32 @@
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
 #include "roc_sndio/backend_map.h"
+#include "roc_sndio/sndfile_helpers.h"
 #include "roc_sndio/sndfile_tables.h"
 #include "roc_status/code_to_str.h"
 
 namespace roc {
 namespace sndio {
 
-namespace {
-
-int pcm_2_sf(audio::PcmFormat fmt) {
-    switch (fmt) {
-    case audio::PcmFormat_UInt8:
-    case audio::PcmFormat_UInt8_Le:
-    case audio::PcmFormat_UInt8_Be:
-        return SF_FORMAT_PCM_U8 | SF_ENDIAN_FILE;
-
-    case audio::PcmFormat_SInt8:
-    case audio::PcmFormat_SInt8_Le:
-    case audio::PcmFormat_SInt8_Be:
-        return SF_FORMAT_PCM_S8 | SF_ENDIAN_FILE;
-
-    case audio::PcmFormat_SInt16:
-        return SF_FORMAT_PCM_16 | SF_ENDIAN_FILE;
-    case audio::PcmFormat_SInt16_Le:
-        return SF_FORMAT_PCM_16 | SF_ENDIAN_LITTLE;
-    case audio::PcmFormat_SInt16_Be:
-        return SF_FORMAT_PCM_16 | SF_ENDIAN_BIG;
-
-    case audio::PcmFormat_SInt24:
-        return SF_FORMAT_PCM_24 | SF_ENDIAN_FILE;
-    case audio::PcmFormat_SInt24_Le:
-        return SF_FORMAT_PCM_24 | SF_ENDIAN_LITTLE;
-    case audio::PcmFormat_SInt24_Be:
-        return SF_FORMAT_PCM_24 | SF_ENDIAN_BIG;
-
-    case audio::PcmFormat_SInt32:
-        return SF_FORMAT_PCM_32 | SF_ENDIAN_FILE;
-    case audio::PcmFormat_SInt32_Le:
-        return SF_FORMAT_PCM_32 | SF_ENDIAN_LITTLE;
-    case audio::PcmFormat_SInt32_Be:
-        return SF_FORMAT_PCM_32 | SF_ENDIAN_BIG;
-
-    case audio::PcmFormat_Float32:
-        return SF_FORMAT_FLOAT | SF_ENDIAN_FILE;
-    case audio::PcmFormat_Float32_Le:
-        return SF_FORMAT_FLOAT | SF_ENDIAN_LITTLE;
-    case audio::PcmFormat_Float32_Be:
-        return SF_FORMAT_FLOAT | SF_ENDIAN_BIG;
-
-    case audio::PcmFormat_Float64:
-        return SF_FORMAT_DOUBLE | SF_ENDIAN_FILE;
-    case audio::PcmFormat_Float64_Le:
-        return SF_FORMAT_DOUBLE | SF_ENDIAN_LITTLE;
-    case audio::PcmFormat_Float64_Be:
-        return SF_FORMAT_DOUBLE | SF_ENDIAN_BIG;
-
-    default:
-        break;
-    }
-
-    return 0;
-}
-
-bool select_major_format(SF_INFO& file_info, const char** driver, const char* path) {
-    roc_panic_if(!driver);
-    roc_panic_if(!path);
-
-    const char* file_extension = NULL;
-
-    const char* dot = strrchr(path, '.');
-    if (dot && dot != path) {
-        file_extension = dot;
-    }
-
-    int format_mask = 0;
-
-    // First try to select format by iterating through sndfile_driver_remap.
-    if (*driver != NULL) {
-        // If driver is non-NULL, match by driver name.
-        for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_driver_remap); idx++) {
-            if (strcmp(*driver, sndfile_driver_remap[idx].driver_name) == 0) {
-                format_mask = sndfile_driver_remap[idx].format_mask;
-                break;
-            }
-        }
-    } else if (file_extension != NULL) {
-        // If driver is NULL, match by file extension.
-        for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_driver_remap); idx++) {
-            if (sndfile_driver_remap[idx].file_extension != NULL) {
-                if (strcmp(file_extension, sndfile_driver_remap[idx].file_extension)
-                    == 0) {
-                    format_mask = sndfile_driver_remap[idx].format_mask;
-                    *driver = file_extension;
-                    break;
-                }
-            }
-        }
-    }
-
-    // Then try to select format by iterating through all sndfile formats.
-    if (format_mask == 0) {
-        int major_count = 0;
-        if (int errnum =
-                sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_count, sizeof(int))) {
-            roc_panic(
-                "sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR_COUNT) failed: %s",
-                sf_error_number(errnum));
-        }
-
-        for (int format_index = 0; format_index < major_count; format_index++) {
-            SF_FORMAT_INFO info;
-            memset(&info, 0, sizeof(info));
-            info.format = format_index;
-            if (int errnum =
-                    sf_command(NULL, SFC_GET_FORMAT_MAJOR, &info, sizeof(info))) {
-                roc_panic("sndfile backend: sf_command(SFC_GET_FORMAT_MAJOR) failed: %s",
-                          sf_error_number(errnum));
-            }
-
-            if (*driver != NULL) {
-                // If driver is non-NULL, match by driver name.
-                if (strcmp(info.extension, *driver) == 0) {
-                    format_mask = info.format;
-                    break;
-                }
-            } else if (file_extension != NULL) {
-                // If driver is NULL, match by file extension.
-                if (strcmp(info.extension, file_extension) == 0) {
-                    format_mask = info.format;
-                    *driver = file_extension;
-                    break;
-                }
-            }
-        }
-    }
-
-    if (format_mask == 0) {
-        roc_log(LogDebug,
-                "sndfile sink: failed to detect major format: driver=%s path=%s", *driver,
-                path);
-        return false;
-    }
-
-    file_info.format = format_mask;
-
-    return true;
-}
-
-bool select_sub_format(SF_INFO& file_info,
-                       const char* driver,
-                       const char* path,
-                       int requested_subformat) {
-    roc_panic_if(!driver);
-    roc_panic_if(!path);
-
-    const int format_mask = file_info.format;
-
-    // User explicitly requested specific PCM format, map it to sub-format.
-    if (requested_subformat != 0) {
-        file_info.format = format_mask | requested_subformat;
-
-        if (sf_format_check(&file_info)) {
-            return true;
-        }
-
-        roc_log(LogError,
-                "sndfile sink: requested format not supported by driver:"
-                " driver=%s path=%s",
-                driver, path);
-        return false;
-    }
-
-    // User did not request specific PCM format.
-    // First check if we can work without sub-format.
-    file_info.format = format_mask;
-
-    if (sf_format_check(&file_info)) {
-        return true;
-    }
-
-    // We can't work without sub-format and should choose one.
-    for (size_t idx = 0; idx < ROC_ARRAY_SIZE(sndfile_default_subformats); idx++) {
-        const int sub_format = sndfile_default_subformats[idx];
-
-        file_info.format = format_mask | sub_format;
-
-        if (sf_format_check(&file_info)) {
-            return true;
-        }
-    }
-
-    roc_log(LogDebug, "sndfile sink: failed to detect sub-format: driver=%s path=%s",
-            driver, path);
-
-    return false;
-}
-
-} // namespace
-
 SndfileSink::SndfileSink(audio::FrameFactory& frame_factory,
                          core::IArena& arena,
-                         const IoConfig& io_config)
+                         const IoConfig& io_config,
+                         const char* path)
     : IDevice(arena)
     , ISink(arena)
     , file_(NULL)
-    , requested_subformat_(0)
     , init_status_(status::NoStatus) {
-    if (io_config.latency != 0) {
-        roc_log(LogError, "sndfile sink: setting io latency not supported by backend");
-        init_status_ = status::StatusBadConfig;
-        return;
-    }
+    file_spec_ = io_config.sample_spec;
+    file_spec_.use_defaults(audio::Format_Invalid, audio::PcmSubformat_Invalid,
+                            audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
+                            audio::ChanMask_Surround_Stereo, 44100);
 
-    if (io_config.sample_spec.sample_format() != audio::SampleFormat_Invalid
-        && io_config.sample_spec.sample_format() != audio::SampleFormat_Pcm) {
-        roc_log(LogError, "sndfile sink: requested format not supported by backend: %s",
-                audio::sample_spec_to_str(sample_spec_).c_str());
-        init_status_ = status::StatusBadConfig;
-        return;
-    }
+    memset(&file_info_, 0, sizeof(file_info_));
 
-    if (io_config.sample_spec.is_pcm()) {
-        // Remember which format to use for file, if requested explicitly.
-        requested_subformat_ = pcm_2_sf(io_config.sample_spec.pcm_format());
-        if (requested_subformat_ == 0) {
-            roc_log(LogError,
-                    "sndfile sink: requested format not supported by backend: %s",
-                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
-            init_status_ = status::StatusBadConfig;
-            return;
-        }
+    if ((init_status_ = open_(path)) != status::StatusOK) {
+        return;
     }
 
-    sample_spec_ = io_config.sample_spec;
-
-    // Always request raw samples from pipeline.
-    // If the user requested different format, we've remembered it above and will
-    // tell libsndfile to perform conversion to that format when writing file.
-    sample_spec_.set_sample_format(audio::SampleFormat_Pcm);
-    sample_spec_.set_pcm_format(audio::Sample_RawFormat);
-
-    sample_spec_.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                              audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo,
-                              44100);
-
-    memset(&file_info_, 0, sizeof(file_info_));
-
     init_status_ = status::StatusOK;
 }
 
@@ -272,20 +52,6 @@ status::StatusCode SndfileSink::init_status() const {
     return init_status_;
 }
 
-status::StatusCode SndfileSink::open(const char* driver, const char* path) {
-    roc_log(LogDebug, "sndfile sink: opening: driver=%s path=%s", driver, path);
-
-    if (file_) {
-        roc_panic("sndfile sink: can't call open() more than once");
-    }
-
-    if (!path) {
-        roc_panic("sndfile sink: path is null");
-    }
-
-    return open_(driver, path);
-}
-
 DeviceType SndfileSink::type() const {
     return DeviceType_Sink;
 }
@@ -303,7 +69,11 @@ audio::SampleSpec SndfileSink::sample_spec() const {
         roc_panic("sndfile sink: not opened");
     }
 
-    return sample_spec_;
+    return frame_spec_;
+}
+
+core::nanoseconds_t SndfileSink::frame_length() const {
+    return 0;
 }
 
 bool SndfileSink::has_state() const {
@@ -323,6 +93,8 @@ status::StatusCode SndfileSink::write(audio::Frame& frame) {
         roc_panic("sndfile sink: not opened");
     }
 
+    frame_spec_.validate_frame(frame);
+
     audio::sample_t* frame_data = frame.raw_samples();
     sf_count_t frame_size = (sf_count_t)frame.num_raw_samples();
 
@@ -355,37 +127,52 @@ void SndfileSink::dispose() {
     arena().dispose_object(*this);
 }
 
-status::StatusCode SndfileSink::open_(const char* driver, const char* path) {
-    file_info_.channels = (int)sample_spec_.num_channels();
-    file_info_.samplerate = (int)sample_spec_.sample_rate();
+status::StatusCode SndfileSink::open_(const char* path) {
+    roc_log(LogDebug, "sndfile sink: opening: path=%s", path);
 
-    if (!select_major_format(file_info_, &driver, path)) {
-        roc_log(LogDebug, "sndfile sink: can't detect file format: driver=%s path=%s",
-                driver, path);
-        return status::StatusErrFile;
+    file_info_.samplerate = (int)file_spec_.sample_rate();
+    file_info_.channels = (int)file_spec_.num_channels();
+
+    status::StatusCode code = status::NoStatus;
+
+    if ((code = sndfile_select_major_format(file_info_, file_spec_, path))
+        != status::StatusOK) {
+        return code;
     }
 
-    if (!select_sub_format(file_info_, driver, path, requested_subformat_)) {
-        roc_log(LogDebug, "sndfile sink: can't detect file format: driver=%s path=%s",
-                driver, path);
-        return status::StatusErrFile;
+    if ((code = sndfile_select_sub_format(file_info_, file_spec_, path))
+        != status::StatusOK) {
+        return code;
     }
 
-    file_ = sf_open(path, SFM_WRITE, &file_info_);
-    if (!file_) {
-        roc_log(LogDebug, "sndfile sink: %s, can't open file: driver=%s path=%s",
-                sf_strerror(file_), driver, path);
+    if (!(file_ = sf_open(path, SFM_WRITE, &file_info_))) {
+        roc_log(LogError, "sndfile sink: can't open output file: %s",
+                sf_error_number(sf_error(NULL)));
         return status::StatusErrFile;
     }
 
     if (!sf_command(file_, SFC_SET_UPDATE_HEADER_AUTO, NULL, SF_TRUE)) {
-        roc_log(LogDebug, "sndfile sink: %s, can't configure driver: driver=%s path=%s",
-                sf_strerror(file_), driver, path);
+        roc_log(LogError, "sndfile sink: can't open output file: %s", sf_strerror(file_));
         return status::StatusErrFile;
     }
 
+    if (!file_spec_.has_format() || !file_spec_.has_subformat()) {
+        if ((code = sndfile_detect_format(file_info_, file_spec_)) != status::StatusOK) {
+            return code;
+        }
+    }
+
+    file_spec_.set_sample_rate((size_t)file_info_.samplerate);
+    file_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
+    file_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
+    file_spec_.channel_set().set_count((size_t)file_info_.channels);
+
+    frame_spec_ = file_spec_;
+    frame_spec_.set_format(audio::Format_Pcm);
+    frame_spec_.set_pcm_subformat(audio::PcmSubformat_Raw);
+
     roc_log(LogInfo, "sndfile sink: opened output file: %s",
-            audio::sample_spec_to_str(sample_spec_).c_str());
+            audio::sample_spec_to_str(file_spec_).c_str());
 
     return status::StatusOK;
 }
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h
index 46efdefb9..18ca1150c 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_sink.h
@@ -12,8 +12,6 @@
 #ifndef ROC_SNDIO_SNDFILE_SINK_H_
 #define ROC_SNDIO_SNDFILE_SINK_H_
 
-#include <sndfile.h>
-
 #include "roc_audio/frame_factory.h"
 #include "roc_audio/sample_spec.h"
 #include "roc_core/iarena.h"
@@ -21,6 +19,8 @@
 #include "roc_sndio/io_config.h"
 #include "roc_sndio/isink.h"
 
+#include <sndfile.h>
+
 namespace roc {
 namespace sndio {
 
@@ -33,15 +33,13 @@ class SndfileSink : public ISink, public core::NonCopyable<> {
     //! Initialize.
     SndfileSink(audio::FrameFactory& frame_factory,
                 core::IArena& arena,
-                const IoConfig& io_config);
+                const IoConfig& io_config,
+                const char* path);
     ~SndfileSink();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open sink.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* driver, const char* path);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -54,6 +52,9 @@ class SndfileSink : public ISink, public core::NonCopyable<> {
     //! Get sample specification of the sink.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the sink.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the sink supports state updates.
     virtual bool has_state() const;
 
@@ -76,14 +77,14 @@ class SndfileSink : public ISink, public core::NonCopyable<> {
     virtual void dispose();
 
 private:
-    status::StatusCode open_(const char* driver, const char* path);
+    status::StatusCode open_(const char* path);
     status::StatusCode close_();
 
     SNDFILE* file_;
     SF_INFO file_info_;
 
-    audio::SampleSpec sample_spec_;
-    int requested_subformat_;
+    audio::SampleSpec frame_spec_;
+    audio::SampleSpec file_spec_;
 
     status::StatusCode init_status_;
 };
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp
index 99166300b..4df5a0ba0 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.cpp
@@ -11,6 +11,7 @@
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
 #include "roc_sndio/backend_map.h"
+#include "roc_sndio/sndfile_helpers.h"
 #include "roc_status/code_to_str.h"
 
 namespace roc {
@@ -18,27 +19,28 @@ namespace sndio {
 
 SndfileSource::SndfileSource(audio::FrameFactory& frame_factory,
                              core::IArena& arena,
-                             const IoConfig& io_config)
+                             const IoConfig& io_config,
+                             const char* path)
     : IDevice(arena)
     , ISource(arena)
     , frame_factory_(frame_factory)
     , file_(NULL)
     , path_(arena)
     , init_status_(status::NoStatus) {
-    if (io_config.latency != 0) {
-        roc_log(LogError, "sndfile source: setting io latency not supported by backend");
-        init_status_ = status::StatusBadConfig;
+    file_spec_ = io_config.sample_spec;
+
+    memset(&file_info_, 0, sizeof(file_info_));
+
+    if (!path_.assign(path)) {
+        roc_log(LogError, "sndfile source: can't allocate string");
+        init_status_ = status::StatusNoMem;
         return;
     }
 
-    if (!io_config.sample_spec.is_empty()) {
-        roc_log(LogError, "sndfile source: setting io encoding not supported by backend");
-        init_status_ = status::StatusBadConfig;
+    if ((init_status_ = open_()) != status::StatusOK) {
         return;
     }
 
-    memset(&file_info_, 0, sizeof(file_info_));
-
     init_status_ = status::StatusOK;
 }
 
@@ -54,25 +56,6 @@ status::StatusCode SndfileSource::init_status() const {
     return init_status_;
 }
 
-status::StatusCode SndfileSource::open(const char* driver, const char* path) {
-    roc_log(LogDebug, "sndfile source: opening: driver=%s path=%s", driver, path);
-
-    if (file_) {
-        roc_panic("sndfile source: can't call open() more than once");
-    }
-
-    if (!path) {
-        roc_panic("sndfile sink: path is null");
-    }
-
-    if (!path_.assign(path)) {
-        roc_log(LogError, "sndfile source: can't allocate string");
-        return status::StatusNoMem;
-    }
-
-    return open_();
-}
-
 DeviceType SndfileSource::type() const {
     return DeviceType_Source;
 }
@@ -90,7 +73,11 @@ audio::SampleSpec SndfileSource::sample_spec() const {
         roc_panic("sndfile source: not opened");
     }
 
-    return sample_spec_;
+    return frame_spec_;
+}
+
+core::nanoseconds_t SndfileSource::frame_length() const {
+    return 0;
 }
 
 bool SndfileSource::has_state() const {
@@ -134,7 +121,7 @@ status::StatusCode SndfileSource::read(audio::Frame& frame,
     }
 
     if (!frame_factory_.reallocate_frame(
-            frame, sample_spec_.stream_timestamp_2_bytes(duration))) {
+            frame, frame_spec_.stream_timestamp_2_bytes(duration))) {
         return status::StatusNoMem;
     }
 
@@ -151,11 +138,12 @@ status::StatusCode SndfileSource::read(audio::Frame& frame,
     }
 
     if (n_samples == 0) {
+        roc_log(LogDebug, "sndfile source: got eof from input file");
         return status::StatusFinish;
     }
 
     frame.set_num_raw_samples((size_t)n_samples);
-    frame.set_duration((size_t)n_samples / sample_spec_.num_channels());
+    frame.set_duration((size_t)n_samples / frame_spec_.num_channels());
 
     if (frame.duration() < duration) {
         return status::StatusPart;
@@ -189,28 +177,69 @@ status::StatusCode SndfileSource::seek_(size_t offset) {
 }
 
 status::StatusCode SndfileSource::open_() {
-    if (file_) {
-        roc_panic("sndfile source: can't open: already opened");
+    roc_log(LogDebug, "sndfile source: opening: path=%s", path_.c_str());
+
+    file_info_.samplerate = (int)file_spec_.sample_rate();
+    file_info_.channels = (int)file_spec_.num_channels();
+
+    status::StatusCode code = status::NoStatus;
+
+    if (file_spec_.has_format()) {
+        if ((code = sndfile_select_major_format(file_info_, file_spec_, path_.c_str()))
+            != status::StatusOK) {
+            return code;
+        }
     }
 
-    file_info_.format = 0;
+    if ((code = sndfile_check_input_spec(file_info_, file_spec_, path_.c_str()))
+        != status::StatusOK) {
+        return code;
+    }
 
-    file_ = sf_open(path_.c_str(), SFM_READ, &file_info_);
-    if (!file_) {
-        roc_log(LogInfo, "sndfile source: can't open: input=%s, %s", path_.c_str(),
-                sf_strerror(file_));
+    if (file_spec_.has_subformat()) {
+        if ((code = sndfile_select_sub_format(file_info_, file_spec_, path_.c_str()))
+            != status::StatusOK) {
+            return code;
+        }
+    }
+
+    const int requested_format = file_info_.format;
+
+    if (!(file_ = sf_open(path_.c_str(), SFM_READ, &file_info_))) {
+        const int err = sf_error(NULL);
+        if (err == SF_ERR_UNRECOGNISED_FORMAT || err == SF_ERR_UNSUPPORTED_ENCODING) {
+            roc_log(LogDebug, "sndfile source: can't recognize input file format");
+            return status::StatusNoFormat;
+        }
+        roc_log(LogError, "sndfile source: can't open input file: %s",
+                sf_error_number(err));
         return status::StatusErrFile;
     }
 
-    sample_spec_.set_sample_format(audio::SampleFormat_Pcm);
-    sample_spec_.set_pcm_format(audio::Sample_RawFormat);
-    sample_spec_.set_sample_rate((size_t)file_info_.samplerate);
-    sample_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
-    sample_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
-    sample_spec_.channel_set().set_count((size_t)file_info_.channels);
+    if ((file_info_.format & requested_format) != requested_format) {
+        roc_log(LogError,
+                "sndfile source: input file doesn't match requested format '%s'",
+                file_spec_.format_name());
+        return status::StatusErrFile;
+    }
+
+    if (!file_spec_.has_format() || !file_spec_.has_subformat()) {
+        if ((code = sndfile_detect_format(file_info_, file_spec_)) != status::StatusOK) {
+            return code;
+        }
+    }
+
+    file_spec_.set_sample_rate((size_t)file_info_.samplerate);
+    file_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
+    file_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
+    file_spec_.channel_set().set_count((size_t)file_info_.channels);
+
+    frame_spec_ = file_spec_;
+    frame_spec_.set_format(audio::Format_Pcm);
+    frame_spec_.set_pcm_subformat(audio::PcmSubformat_Raw);
 
     roc_log(LogInfo, "sndfile source: opened input file: %s",
-            audio::sample_spec_to_str(sample_spec_).c_str());
+            audio::sample_spec_to_str(file_spec_).c_str());
 
     return status::StatusOK;
 }
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h
index e84aa9b61..8652a2d92 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_source.h
@@ -12,8 +12,6 @@
 #ifndef ROC_SNDIO_SNDFILE_SOURCE_H_
 #define ROC_SNDIO_SNDFILE_SOURCE_H_
 
-#include <sndfile.h>
-
 #include "roc_audio/frame_factory.h"
 #include "roc_audio/sample_spec.h"
 #include "roc_core/iarena.h"
@@ -22,6 +20,8 @@
 #include "roc_sndio/io_config.h"
 #include "roc_sndio/isource.h"
 
+#include <sndfile.h>
+
 namespace roc {
 namespace sndio {
 
@@ -34,15 +34,13 @@ class SndfileSource : public ISource, private core::NonCopyable<> {
     //! Initialize.
     SndfileSource(audio::FrameFactory& frame_factory,
                   core::IArena& arena,
-                  const IoConfig& io_config);
+                  const IoConfig& io_config,
+                  const char* path);
     ~SndfileSource();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open source.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* driver, const char* path);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -55,6 +53,9 @@ class SndfileSource : public ISource, private core::NonCopyable<> {
     //! Get sample specification of the source.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the source.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the source supports state updates.
     virtual bool has_state() const;
 
@@ -90,7 +91,8 @@ class SndfileSource : public ISource, private core::NonCopyable<> {
 
     audio::FrameFactory& frame_factory_;
 
-    audio::SampleSpec sample_spec_;
+    audio::SampleSpec frame_spec_;
+    audio::SampleSpec file_spec_;
 
     SNDFILE* file_;
     SF_INFO file_info_;
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.cpp b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.cpp
index 1dbf71cf8..753b07b50 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.cpp
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.cpp
@@ -33,7 +33,8 @@ enum {
 
 } // namespace
 
-SndfileDriverInfo sndfile_driver_remap[ROC_ARRAY_SIZE(sndfile_driver_remap)] = {
+SndfileFormatInfo sndfile_format_remap[ROC_ARRAY_SIZE(sndfile_format_remap)] = {
+    { "pcm", NULL, SF_FORMAT_RAW },
     { "ogg", ".ogg", SF_FORMAT_OGG },
     { "mp1", ".mp1", SF_FORMAT_MPEG | SF_FORMAT_MPEG_LAYER_I },
     { "mp2", ".mp2", SF_FORMAT_MPEG | SF_FORMAT_MPEG_LAYER_II },
@@ -44,8 +45,36 @@ SndfileDriverInfo sndfile_driver_remap[ROC_ARRAY_SIZE(sndfile_driver_remap)] = {
     { "wavex", NULL, SF_FORMAT_WAVEX },
 };
 
+SndfileSubformatInfo sndfile_subformat_map[ROC_ARRAY_SIZE(sndfile_subformat_map)] = {
+    // lpcm
+    { "lpcm", "ulaw", SF_FORMAT_ULAW },
+    { "lpcm", "alaw", SF_FORMAT_ALAW },
+    // dpcm
+    { "dpcm", "d8", SF_FORMAT_DPCM_8 },
+    { "dpcm", "d16", SF_FORMAT_DPCM_16 },
+    // adpcm
+    { "adpcm", "adpcm_ima", SF_FORMAT_IMA_ADPCM },
+    { "adpcm", "adpcm_ms", SF_FORMAT_MS_ADPCM },
+    { "adpcm", "adpcm_vox", SF_FORMAT_VOX_ADPCM },
+    // dwvw
+    { "dwvw", "dwvw12", SF_FORMAT_DWVW_12 },
+    { "dwvw", "dwvw16", SF_FORMAT_DWVW_16 },
+    { "dwvw", "dwvw24", SF_FORMAT_DWVW_24 },
+    // g72x
+    { "g72x", "g721_32", SF_FORMAT_G721_32 },
+    { "g72x", "g723_24", SF_FORMAT_G723_24 },
+    { "g72x", "g723_40", SF_FORMAT_G723_40 },
+    // ogg
+    { "ogg", "vorbis", SF_FORMAT_VORBIS },
+    { "ogg", "opus", SF_FORMAT_OPUS },
+};
+
+int sndfile_explicit_formats[ROC_ARRAY_SIZE(sndfile_explicit_formats)] = {
+    SF_FORMAT_RAW, // pcm
+};
+
 int sndfile_default_subformats[ROC_ARRAY_SIZE(sndfile_default_subformats)] = {
-    // at least one of the PCM sub-formats is supported by almost every major format
+    // most major formats supports at least one PCM or DPCM sub-format
     SF_FORMAT_PCM_24,
     SF_FORMAT_PCM_16,
     SF_FORMAT_DPCM_16,
diff --git a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.h b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.h
index 266268025..b7704f71e 100644
--- a/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.h
+++ b/src/internal_modules/roc_sndio/target_sndfile/roc_sndio/sndfile_tables.h
@@ -15,22 +15,40 @@
 namespace roc {
 namespace sndio {
 
-//! Sndfile driver meta-data.
-struct SndfileDriverInfo {
-    //! Name of the driver.
-    const char* driver_name;
-    //! File extension associated with driver.
+//! Sndfile format meta-data.
+struct SndfileFormatInfo {
+    //! Name of the format.
+    const char* name;
+    //! File extension associated with the format.
     const char* file_extension;
     //! SF_FORMAT corresponding to the driver.
     int format_mask;
 };
 
-//! Table of sndfile drivers with re-mapped names or file extensions.
-//! This table should be checked when we need to guess format from driver
-//! name or file extension.
-extern SndfileDriverInfo sndfile_driver_remap[9];
+//! Sndfile driver meta-data.
+struct SndfileSubformatInfo {
+    //! Name of sub-format group.
+    const char* group;
+    //! Name of sub-format.
+    const char* name;
+    //! SF_FORMAT corresponding to the sub-format.
+    int format_mask;
+};
+
+//! Table of sndfile formats with re-mapped names or file extensions.
+//! This table is checked when user explicitly specifies format name,
+//! or we're trying to guess format from file extension.
+extern SndfileFormatInfo sndfile_format_remap[9];
+
+//! Table of sndfile sub-formats with mapped string names and divided into groups.
+//! This table is checked when user explicitly specifies sub-format name.
+extern SndfileSubformatInfo sndfile_subformat_map[15];
+
+//! Table of sndfile formats which require explicitly providing sub-format,
+//! rate, and channels.
+extern int sndfile_explicit_formats[1];
 
-//! Table of sndfile sub-formats to try when no specific format requested.
+//! Table of sndfile sub-formats to try when no specific sub-format requested.
 //! This list provides the minimum number of sub-formats needed to support
 //! all possible major formats.
 extern int sndfile_default_subformats[7];
diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp
index 804f6a4ec..4fd248e70 100644
--- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp
+++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp
@@ -31,117 +31,80 @@ const char* default_drivers[] = {
 };
 
 const char* driver_renames[][2] = {
+    // device drivers
     { "waveaudio", "wave" },
     { "coreaudio", "core" },
+    // file formats
+    { "anb", "amr" },
 };
 
 const char* hidden_drivers[] = {
-    // raw format aliases
-    "f4",
-    "f8",
-    "s1",
-    "s2",
-    "s3",
-    "s4",
-    "u1",
-    "u2",
-    "u3",
-    "u4",
-    "sb",
-    "sw",
-    "sl",
-    "ub",
-    "uw",
-    "f32",
-    "f64",
-    "s8",
-    "s16",
-    "s24",
-    "s32",
-    "u8",
-    "u16",
-    "u24",
-    "u32",
-    // formats already handled by libsndfile
-    "aif",
-    "aifc",
-    "aiff",
-    "aiffc",
-    "mat",
-    "mat4",
-    "mat5",
-    "vorbis",
-    // pseudo-formats
-    "sndfile",
-    "null",
-    "wavpcm",
-    // unsupported device drivers
     "ao",
     "ossdsp",
     "pulseaudio",
 };
 
-bool is_default_driver(const char* driver) {
-    for (size_t n = 0; n < ROC_ARRAY_SIZE(default_drivers); n++) {
-        if (strcmp(driver, default_drivers[n]) == 0) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
-const char* map_to_sox_driver(const char* driver) {
-    if (!driver) {
+const char* driver_to_sox(const char* name) {
+    if (!name) {
         return NULL;
     }
     for (size_t n = 0; n < ROC_ARRAY_SIZE(driver_renames); n++) {
-        if (strcmp(driver_renames[n][1], driver) == 0) {
+        if (strcmp(driver_renames[n][1], name) == 0) {
             return driver_renames[n][0];
         }
     }
-    return driver;
+    return name;
 }
 
-const char* map_from_sox_driver(const char* driver) {
-    if (!driver) {
+const char* driver_from_sox(const char* name) {
+    if (!name) {
         return NULL;
     }
     for (size_t n = 0; n < ROC_ARRAY_SIZE(driver_renames); n++) {
-        if (strcmp(driver_renames[n][0], driver) == 0) {
+        if (strcmp(driver_renames[n][0], name) == 0) {
             return driver_renames[n][1];
         }
     }
-    return driver;
+    return name;
 }
 
-bool is_driver_hidden(const char* driver) {
-    // replicate the behavior of display_supported_formats() from sox.c
-    if (strchr(driver, '/')) {
-        return true;
-    }
-    for (size_t n = 0; n < ROC_ARRAY_SIZE(hidden_drivers); n++) {
-        if (strcmp(hidden_drivers[n], driver) == 0) {
+bool is_default_driver(const char* name) {
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(default_drivers); n++) {
+        if (strcmp(name, default_drivers[n]) == 0) {
             return true;
         }
     }
     return false;
 }
 
-bool check_handler_type(const sox_format_handler_t* handler, DriverType driver_type) {
-    if (handler->flags & SOX_FILE_DEVICE) {
-        if (handler->flags & SOX_FILE_PHONY) {
-            return false;
-        }
-        if (driver_type != DriverType_Device) {
-            return false;
-        }
-    } else {
-        if (driver_type != DriverType_File) {
+bool is_supported_driver(const char* name) {
+    const sox_format_handler_t* format_handler = sox_write_handler(NULL, name, NULL);
+    if (format_handler == NULL) {
+        // not enabled in sox
+        return false;
+    }
+    if (!(format_handler->flags & SOX_FILE_DEVICE)) {
+        // not device
+        return false;
+    }
+    if (format_handler->flags & SOX_FILE_PHONY) {
+        // phony device
+        return false;
+    }
+
+    if (strchr(name, '/')) {
+        // replicate the behavior of display_supported_formats() from sox.c
+        return false;
+    }
+
+    for (size_t n = 0; n < ROC_ARRAY_SIZE(hidden_drivers); n++) {
+        // hidden by us
+        if (strcmp(hidden_drivers[n], name) == 0) {
             return false;
         }
     }
 
+    // supported!
     return true;
 }
 
@@ -176,8 +139,7 @@ void log_handler(unsigned sox_level,
 
 } // namespace
 
-SoxBackend::SoxBackend()
-    : first_created_(false) {
+SoxBackend::SoxBackend() {
     sox_init();
 
     sox_get_globals()->verbosity = 100;
@@ -188,129 +150,120 @@ const char* SoxBackend::name() const {
     return "sox";
 }
 
-void SoxBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list) {
+bool SoxBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& result) {
     for (size_t n = 0; n < ROC_ARRAY_SIZE(default_drivers); n++) {
-        const sox_format_handler_t* handler =
-            sox_write_handler(NULL, default_drivers[n], NULL);
-        if (!handler) {
+        const char* driver = default_drivers[n];
+        if (!is_supported_driver(driver)) {
             continue;
         }
 
-        const char* driver = map_from_sox_driver(default_drivers[n]);
-
-        if (!driver_list.push_back(DriverInfo(driver, DriverType_Device,
-                                              DriverFlag_IsDefault
-                                                  | DriverFlag_SupportsSource
-                                                  | DriverFlag_SupportsSink,
-                                              this))) {
-            roc_panic("sox backend: can't add driver");
+        if (!result.push_back(DriverInfo(driver_from_sox(driver),
+                                         Driver_Device | Driver_DefaultDevice
+                                             | Driver_SupportsSource
+                                             | Driver_SupportsSink,
+                                         this))) {
+            return false;
         }
     }
 
     const sox_format_tab_t* formats = sox_get_format_fns();
-
     for (size_t n = 0; formats[n].fn; n++) {
-        sox_format_handler_t const* handler = formats[n].fn();
-
+        sox_format_handler_t const* format_handler = formats[n].fn();
         char const* const* format_names;
-        for (format_names = handler->names; *format_names; ++format_names) {
-            const char* driver = map_from_sox_driver(*format_names);
 
-            if (is_driver_hidden(driver) || is_default_driver(driver)) {
+        for (format_names = format_handler->names; *format_names; ++format_names) {
+            const char* driver = *format_names;
+            if (!is_supported_driver(driver) || is_default_driver(driver)) {
                 continue;
             }
 
-            if (!driver_list.push_back(DriverInfo(
-                    driver,
-                    (handler->flags & SOX_FILE_DEVICE) ? DriverType_Device
-                                                       : DriverType_File,
-                    DriverFlag_SupportsSource | DriverFlag_SupportsSink, this))) {
-                roc_panic("sox backend: can't add driver");
+            if (!result.push_back(DriverInfo(
+                    driver_from_sox(driver),
+                    Driver_Device | Driver_SupportsSource | Driver_SupportsSink, this))) {
+                return false;
             }
         }
     }
+
+    return true;
+}
+
+bool SoxBackend::discover_formats(core::Array<FormatInfo, MaxFormats>& result) {
+    // no formats except pcm
+    return true;
+}
+
+bool SoxBackend::discover_subformat_groups(core::StringList& result) {
+    // no sub-formats except pcm
+    return true;
+}
+
+bool SoxBackend::discover_subformats(const char* group, core::StringList& result) {
+    // no sub-formats except pcm
+    return true;
 }
 
 status::StatusCode SoxBackend::open_device(DeviceType device_type,
-                                           DriverType driver_type,
                                            const char* driver,
                                            const char* path,
                                            const IoConfig& io_config,
                                            audio::FrameFactory& frame_factory,
                                            core::IArena& arena,
                                            IDevice** result) {
-    first_created_ = true;
-
-    driver = map_to_sox_driver(driver);
-
-    if (driver && is_driver_hidden(driver)) {
-        roc_log(LogDebug, "sox backend: driver is not supported: driver=%s path=%s",
-                driver, path);
-        return status::StatusNoDriver;
-    }
-
-    const sox_format_handler_t* handler = sox_write_handler(path, driver, NULL);
-    if (!handler) {
-        roc_log(LogDebug, "sox backend: driver is not available: driver=%s path=%s",
-                driver, path);
-        return status::StatusNoDriver;
-    }
-
-    if (!check_handler_type(handler, driver_type)) {
-        roc_log(LogDebug, "sox backend: mismatching driver type: driver=%s path=%s",
-                driver, path);
-        return status::StatusNoDriver;
+    roc_panic_if(!driver);
+    roc_panic_if(!path);
+
+    if (driver) {
+        driver = driver_to_sox(driver);
+
+        if (!is_supported_driver(driver)) {
+            roc_log(LogDebug,
+                    "sox backend sink: requested driver not supported by backend:"
+                    " driver=%s path=%s",
+                    driver, path);
+            // Try another backend.
+            return status::StatusNoDriver;
+        }
     }
 
     switch (device_type) {
     case DeviceType_Sink: {
         core::ScopedPtr<SoxSink> sink(
-            new (arena) SoxSink(frame_factory, arena, io_config, driver_type));
+            new (arena) SoxSink(frame_factory, arena, io_config, driver, path));
 
         if (!sink) {
-            roc_log(LogDebug, "sox backend: can't allocate sink: path=%s", path);
+            roc_log(LogDebug, "sox backend: can't allocate sink: driver=%s path=%s",
+                    driver, path);
             return status::StatusNoMem;
         }
 
         if (sink->init_status() != status::StatusOK) {
-            roc_log(LogDebug, "sox backend: can't initialize sink: path=%s status=%s",
-                    path, status::code_to_str(sink->init_status()));
+            roc_log(LogDebug, "sox backend: can't open sink: driver=%s path=%s status=%s",
+                    driver, path, status::code_to_str(sink->init_status()));
             return sink->init_status();
         }
 
-        const status::StatusCode code = sink->open(driver, path);
-        if (code != status::StatusOK) {
-            roc_log(LogDebug, "sox backend: can't open sink: path=%s status=%s", path,
-                    status::code_to_str(code));
-            return code;
-        }
-
         *result = sink.hijack();
         return status::StatusOK;
     } break;
 
     case DeviceType_Source: {
         core::ScopedPtr<SoxSource> source(
-            new (arena) SoxSource(frame_factory, arena, io_config, driver_type));
+            new (arena) SoxSource(frame_factory, arena, io_config, driver, path));
 
         if (!source) {
-            roc_log(LogDebug, "sox backend: can't allocate source: path=%s", path);
+            roc_log(LogDebug, "sox backend: can't allocate source: driver=%s path=%s",
+                    driver, path);
             return status::StatusNoMem;
         }
 
         if (source->init_status() != status::StatusOK) {
-            roc_log(LogDebug, "sox backend: can't initialize source: path=%s status=%s",
+            roc_log(LogDebug,
+                    "sox backend: can't open source: driver=%s path=%s status=%s", driver,
                     path, status::code_to_str(source->init_status()));
             return source->init_status();
         }
 
-        const status::StatusCode code = source->open(driver, path);
-        if (code != status::StatusOK) {
-            roc_log(LogDebug, "sox backend: can't open source: path=%s status=%s", path,
-                    status::code_to_str(code));
-            return code;
-        }
-
         *result = source.hijack();
         return status::StatusOK;
     } break;
diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h
index 0b2442bfa..c939b57a1 100644
--- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h
+++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h
@@ -29,21 +29,29 @@ class SoxBackend : public IBackend, core::NonCopyable<> {
     virtual const char* name() const;
 
     //! Append supported drivers to the list.
-    virtual void discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list);
+    virtual ROC_ATTR_NODISCARD bool
+    discover_drivers(core::Array<DriverInfo, MaxDrivers>& result);
+
+    //! Append supported formats to the list.
+    virtual ROC_ATTR_NODISCARD bool
+    discover_formats(core::Array<FormatInfo, MaxFormats>& result);
+
+    //! Append supported groups of sub-formats to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformat_groups(core::StringList& result);
+
+    //! Append supported sub-formats of a group to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformats(const char* group,
+                                                        core::StringList& result);
 
     //! Create and open a sink or source.
     virtual ROC_ATTR_NODISCARD status::StatusCode
     open_device(DeviceType device_type,
-                DriverType driver_type,
                 const char* driver,
                 const char* path,
                 const IoConfig& io_config,
                 audio::FrameFactory& frame_factory,
                 core::IArena& arena,
                 IDevice** result);
-
-private:
-    bool first_created_;
 };
 
 } // namespace sndio
diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp
index ad1562617..126ee2632 100644
--- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp
+++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp
@@ -7,6 +7,7 @@
  */
 
 #include "roc_sndio/sox_sink.h"
+#include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
 #include "roc_sndio/backend_map.h"
@@ -15,15 +16,21 @@
 namespace roc {
 namespace sndio {
 
+namespace {
+
+const core::nanoseconds_t DefaultFrameLength = 10 * core::Millisecond;
+
+} // namespace
+
 SoxSink::SoxSink(audio::FrameFactory& frame_factory,
                  core::IArena& arena,
                  const IoConfig& io_config,
-                 DriverType driver_type)
+                 const char* driver,
+                 const char* path)
     : IDevice(arena)
     , ISink(arena)
-    , driver_type_(driver_type)
-    , driver_name_(arena)
-    , output_name_(arena)
+    , driver_(arena)
+    , path_(arena)
     , output_(NULL)
     , buffer_(arena)
     , buffer_size_(0)
@@ -32,51 +39,89 @@ SoxSink::SoxSink(audio::FrameFactory& frame_factory,
     BackendMap::instance();
 
     if (io_config.latency != 0) {
-        roc_log(LogError, "sox sink: setting io latency not supported by sox backend");
+        roc_log(LogError, "sox sink: setting io latency not implemented for sox backend");
         init_status_ = status::StatusBadConfig;
         return;
     }
 
-    sample_spec_ = io_config.sample_spec;
+    if (io_config.sample_spec.has_format()
+        && io_config.sample_spec.format() != audio::Format_Pcm) {
+        roc_log(LogError,
+                "sox sink: invalid io encoding:"
+                " <format> '%s' not supported by backend: spec=%s",
+                io_config.sample_spec.format_name(),
+                audio::sample_spec_to_str(io_config.sample_spec).c_str());
+        init_status_ = status::StatusBadConfig;
+        return;
+    }
 
-    if (driver_type_ == DriverType_File) {
-        sample_spec_.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                                  audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo,
-                                  44100);
-    } else {
-        sample_spec_.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                                  audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo,
-                                  0);
+    if (io_config.sample_spec.has_subformat()) {
+        if (io_config.sample_spec.pcm_subformat() == audio::PcmSubformat_Invalid) {
+            roc_log(LogError,
+                    "sox sink: invalid io encoding:"
+                    " <subformat> '%s' not supported by backend: spec=%s",
+                    io_config.sample_spec.subformat_name(),
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
+
+        const audio::PcmTraits subfmt =
+            audio::pcm_subformat_traits(io_config.sample_spec.pcm_subformat());
+
+        if (!subfmt.has_flags(audio::Pcm_IsInteger | audio::Pcm_IsSigned)) {
+            roc_log(LogError,
+                    "sox sink: invalid io encoding:"
+                    " <subformat> must be signed integer (like s16): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
+
+        if (!subfmt.has_flags(audio::Pcm_IsPacked | audio::Pcm_IsAligned)) {
+            roc_log(LogError,
+                    "sox sink: invalid io encoding:"
+                    " <subformat> must be packed (like s24, not s24_4) and byte-aligned"
+                    " (like s16, not s18): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
+
+        if (io_config.sample_spec.pcm_subformat() != subfmt.default_variant) {
+            roc_log(LogError,
+                    "sox sink: invalid io encoding:"
+                    " <subformat> must be default-endian (like s16, not s16_le): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
     }
 
-    if (!sample_spec_.is_raw()) {
-        roc_log(LogError, "sox sink: sample format can be only \"-\" or \"%s\"",
-                audio::pcm_format_to_str(audio::Sample_RawFormat));
-        init_status_ = status::StatusBadConfig;
-        return;
+    out_spec_ = io_config.sample_spec;
+    if (!out_spec_.has_format()) {
+        out_spec_.set_format(audio::Format_Pcm);
+        out_spec_.set_pcm_subformat(audio::PcmSubformat_SInt16);
     }
 
     frame_length_ = io_config.frame_length;
-
     if (frame_length_ == 0) {
-        roc_log(LogError, "sox sink: frame length is zero");
-        init_status_ = status::StatusBadConfig;
-        return;
+        frame_length_ = DefaultFrameLength;
     }
 
-    {
-        audio::SampleSpec spec = sample_spec_;
-        spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                          audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo, 44100);
+    roc_log(LogDebug, "sox sink: opening: driver=%s path=%s", driver, path);
 
-        sox_get_globals()->bufsiz =
-            spec.ns_2_samples_overall(frame_length_) * sizeof(sox_sample_t);
+    if ((init_status_ = init_names_(driver, path)) != status::StatusOK) {
+        return;
     }
 
-    memset(&out_signal_, 0, sizeof(out_signal_));
-    out_signal_.rate = (sox_rate_t)sample_spec_.sample_rate();
-    out_signal_.channels = (unsigned)sample_spec_.num_channels();
-    out_signal_.precision = SOX_SAMPLE_PRECISION;
+    if ((init_status_ = open_()) != status::StatusOK) {
+        return;
+    }
+
+    if ((init_status_ = init_buffer_()) != status::StatusOK) {
+        return;
+    }
 
     init_status_ = status::StatusOK;
 }
@@ -92,30 +137,6 @@ status::StatusCode SoxSink::init_status() const {
     return init_status_;
 }
 
-status::StatusCode SoxSink::open(const char* driver, const char* path) {
-    roc_log(LogDebug, "sox sink: opening: driver=%s path=%s", driver, path);
-
-    if (buffer_.size() != 0 || output_) {
-        roc_panic("sox sink: can't call open() more than once");
-    }
-
-    status::StatusCode code = status::NoStatus;
-
-    if ((code = init_names_(driver, path)) != status::StatusOK) {
-        return code;
-    }
-
-    if ((code = open_()) != status::StatusOK) {
-        return code;
-    }
-
-    if ((code = init_buffer_()) != status::StatusOK) {
-        return code;
-    }
-
-    return status::StatusOK;
-}
-
 DeviceType SoxSink::type() const {
     return DeviceType_Sink;
 }
@@ -129,15 +150,15 @@ ISource* SoxSink::to_source() {
 }
 
 audio::SampleSpec SoxSink::sample_spec() const {
-    if (!output_) {
-        roc_panic("sox sink: not opened");
-    }
+    return frame_spec_;
+}
 
-    return sample_spec_;
+core::nanoseconds_t SoxSink::frame_length() const {
+    return frame_length_;
 }
 
 bool SoxSink::has_state() const {
-    return driver_type_ == DriverType_Device;
+    return true;
 }
 
 DeviceState SoxSink::state() const {
@@ -157,14 +178,12 @@ status::StatusCode SoxSink::pause() {
         roc_panic("sox sink: not opened");
     }
 
-    roc_log(LogDebug, "sox sink: pausing: driver=%s output=%s", driver_name_.c_str(),
-            output_name_.c_str());
+    roc_log(LogDebug, "sox sink: pausing: driver=%s path=%s", driver_.c_str(),
+            path_.c_str());
 
-    if (driver_type_ == DriverType_Device) {
-        const status::StatusCode close_code = close_();
-        if (close_code != status::StatusOK) {
-            return close_code;
-        }
+    const status::StatusCode close_code = close_();
+    if (close_code != status::StatusOK) {
+        return close_code;
     }
 
     paused_ = true;
@@ -177,8 +196,8 @@ status::StatusCode SoxSink::resume() {
         return status::StatusOK;
     }
 
-    roc_log(LogDebug, "sox sink: resuming: driver=%s output=%s", driver_name_.c_str(),
-            output_name_.c_str());
+    roc_log(LogDebug, "sox sink: resuming: driver=%s path=%s", driver_.c_str(),
+            path_.c_str());
 
     if (!output_) {
         const status::StatusCode code = open_();
@@ -197,10 +216,12 @@ bool SoxSink::has_latency() const {
 }
 
 bool SoxSink::has_clock() const {
-    return driver_type_ == DriverType_Device;
+    return true;
 }
 
 status::StatusCode SoxSink::write(audio::Frame& frame) {
+    frame_spec_.validate_frame(frame);
+
     const audio::sample_t* frame_data = frame.raw_samples();
     size_t frame_size = frame.num_raw_samples();
 
@@ -219,18 +240,18 @@ status::StatusCode SoxSink::write(audio::Frame& frame) {
         }
 
         if (buffer_pos == buffer_size_) {
-            const status::StatusCode code = write_(buffer_data, buffer_pos);
-            if (code != status::StatusOK) {
-                return code;
+            if (sox_write(output_, buffer_data, buffer_pos) != buffer_pos) {
+                roc_log(LogError, "sox sink: failed to write output buffer");
+                return status::StatusErrDevice;
             }
             buffer_pos = 0;
         }
     }
 
     if (buffer_pos > 0) {
-        const status::StatusCode code = write_(buffer_data, buffer_pos);
-        if (code != status::StatusOK) {
-            return code;
+        if (sox_write(output_, buffer_data, buffer_pos) != buffer_pos) {
+            roc_log(LogError, "sox sink: failed to write output buffer");
+            return status::StatusErrDevice;
         }
     }
 
@@ -238,12 +259,6 @@ status::StatusCode SoxSink::write(audio::Frame& frame) {
 }
 
 status::StatusCode SoxSink::flush() {
-    if (output_ != NULL && driver_type_ == DriverType_File && output_->fp != NULL) {
-        if (fflush((FILE*)output_->fp) != 0) {
-            return status::StatusErrFile;
-        }
-    }
-
     return status::StatusOK;
 }
 
@@ -257,14 +272,14 @@ void SoxSink::dispose() {
 
 status::StatusCode SoxSink::init_names_(const char* driver, const char* path) {
     if (driver) {
-        if (!driver_name_.assign(driver)) {
+        if (!driver_.assign(driver)) {
             roc_log(LogError, "sox sink: can't allocate string");
             return status::StatusNoMem;
         }
     }
 
     if (path) {
-        if (!output_name_.assign(path)) {
+        if (!path_.assign(path)) {
             roc_log(LogError, "sox sink: can't allocate string");
             return status::StatusNoMem;
         }
@@ -274,7 +289,7 @@ status::StatusCode SoxSink::init_names_(const char* driver, const char* path) {
 }
 
 status::StatusCode SoxSink::init_buffer_() {
-    buffer_size_ = sample_spec_.ns_2_samples_overall(frame_length_);
+    buffer_size_ = frame_spec_.ns_2_samples_overall(frame_length_);
     if (buffer_size_ == 0) {
         roc_log(LogError, "sox sink: buffer size is zero");
         return status::StatusBadConfig;
@@ -288,14 +303,17 @@ status::StatusCode SoxSink::init_buffer_() {
 }
 
 status::StatusCode SoxSink::open_() {
-    output_ = sox_open_write(
-        output_name_.is_empty() ? NULL : output_name_.c_str(), &out_signal_, NULL,
-        driver_name_.is_empty() ? NULL : driver_name_.c_str(), NULL, NULL);
+    memset(&out_signal_, 0, sizeof(out_signal_));
+    out_signal_.rate = (sox_rate_t)out_spec_.sample_rate();
+    out_signal_.channels = (unsigned)out_spec_.num_channels();
+    out_signal_.precision = (unsigned)out_spec_.pcm_bit_width();
+
+    output_ = sox_open_write(path_.is_empty() ? NULL : path_.c_str(), &out_signal_, NULL,
+                             driver_.is_empty() ? NULL : driver_.c_str(), NULL, NULL);
     if (!output_) {
-        roc_log(LogDebug, "sox sink: can't open: driver=%s path=%s", driver_name_.c_str(),
-                output_name_.c_str());
-        return driver_type_ == DriverType_File ? status::StatusErrFile
-                                               : status::StatusErrDevice;
+        roc_log(LogDebug, "sox sink: can't open: driver=%s path=%s", driver_.c_str(),
+                path_.c_str());
+        return status::StatusErrDevice;
     }
 
     const unsigned long requested_rate = (unsigned long)out_signal_.rate;
@@ -304,11 +322,10 @@ status::StatusCode SoxSink::open_() {
     if (requested_rate != 0 && requested_rate != actual_rate) {
         roc_log(LogError,
                 "sox sink:"
-                " can't open output file or device with the requested sample rate:"
-                " required_by_output=%lu requested_by_user=%lu",
+                " can't open output device with the requested sample rate:"
+                " supported=%lu requested=%lu",
                 actual_rate, requested_rate);
-        return driver_type_ == DriverType_File ? status::StatusErrFile
-                                               : status::StatusErrDevice;
+        return status::StatusErrDevice;
     }
 
     const unsigned long requested_chans = (unsigned long)out_signal_.channels;
@@ -317,35 +334,35 @@ status::StatusCode SoxSink::open_() {
     if (requested_chans != 0 && requested_chans != actual_chans) {
         roc_log(LogError,
                 "sox sink:"
-                " can't open output file or device with the requested channel count:"
-                " required_by_output=%lu requested_by_user=%lu",
+                " can't open output device with the requested channel count:"
+                " supported=%lu requested=%lu",
                 actual_chans, requested_chans);
-        return driver_type_ == DriverType_File ? status::StatusErrFile
-                                               : status::StatusErrDevice;
+        return status::StatusErrDevice;
     }
 
-    sample_spec_.set_sample_rate(actual_rate);
-    sample_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
-    sample_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
-    sample_spec_.channel_set().set_count(actual_chans);
+    const unsigned long requested_bits = (unsigned long)out_signal_.precision;
+    const unsigned long actual_bits = (unsigned long)output_->signal.precision;
+
+    if (requested_bits != 0 && requested_bits != actual_bits) {
+        roc_log(LogError,
+                "sox sink:"
+                " can't open output device with the requested subformat:"
+                " supported=s%lu requested=s%lu",
+                actual_bits, requested_bits);
+        return status::StatusErrDevice;
+    }
 
-    roc_log(LogInfo,
-            "sox sink: opened output:"
-            " bits=%lu rate=%lu req_rate=%lu chans=%lu req_chans=%lu is_file=%d",
-            (unsigned long)output_->encoding.bits_per_sample, actual_rate, requested_rate,
-            actual_chans, requested_chans, (int)(driver_type_ == DriverType_File));
+    out_spec_.set_sample_rate(actual_rate);
+    out_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
+    out_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
+    out_spec_.channel_set().set_count(actual_chans);
 
-    return status::StatusOK;
-}
+    frame_spec_ = out_spec_;
+    frame_spec_.set_format(audio::Format_Pcm);
+    frame_spec_.set_pcm_subformat(audio::PcmSubformat_Raw);
 
-status::StatusCode SoxSink::write_(const sox_sample_t* samples, size_t n_samples) {
-    if (n_samples > 0) {
-        if (sox_write(output_, samples, n_samples) != n_samples) {
-            roc_log(LogError, "sox sink: failed to write output buffer");
-            return driver_type_ == DriverType_File ? status::StatusErrFile
-                                                   : status::StatusErrDevice;
-        }
-    }
+    roc_log(LogInfo, "sox sink: opened output device: %s",
+            audio::sample_spec_to_str(out_spec_).c_str());
 
     return status::StatusOK;
 }
@@ -362,8 +379,7 @@ status::StatusCode SoxSink::close_() {
 
     if (err != SOX_SUCCESS) {
         roc_log(LogError, "sox sink: can't close output: %s", sox_strerror(err));
-        return driver_type_ == DriverType_File ? status::StatusErrFile
-                                               : status::StatusErrDevice;
+        return status::StatusErrDevice;
     }
 
     return status::StatusOK;
diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.h b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.h
index be4215ca9..b99cc7e19 100644
--- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.h
+++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.h
@@ -28,23 +28,22 @@ namespace sndio {
 
 //! SoX sink.
 //! @remarks
-//!  Writes samples to output file or device.
-//!  Supports multiple drivers for different file types and audio systems.
+//!  Writes samples to output device.
+//!  Supports multiple drivers for different audio systems.
+//!  Does not support files.
 class SoxSink : public ISink, public core::NonCopyable<> {
 public:
     //! Initialize.
     SoxSink(audio::FrameFactory& frame_factory,
             core::IArena& arena,
             const IoConfig& io_config,
-            DriverType driver_type);
+            const char* driver,
+            const char* path);
     ~SoxSink();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open sink.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* driver, const char* path);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -57,6 +56,9 @@ class SoxSink : public ISink, public core::NonCopyable<> {
     //! Get sample specification of the sink.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the sink.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the sink supports state updates.
     virtual bool has_state() const;
 
@@ -95,9 +97,8 @@ class SoxSink : public ISink, public core::NonCopyable<> {
     status::StatusCode write_(const sox_sample_t* samples, size_t n_samples);
     status::StatusCode close_();
 
-    const DriverType driver_type_;
-    core::StringBuffer driver_name_;
-    core::StringBuffer output_name_;
+    core::StringBuffer driver_;
+    core::StringBuffer path_;
 
     sox_format_t* output_;
     sox_signalinfo_t out_signal_;
@@ -105,7 +106,9 @@ class SoxSink : public ISink, public core::NonCopyable<> {
     core::Array<sox_sample_t> buffer_;
     size_t buffer_size_;
     core::nanoseconds_t frame_length_;
-    audio::SampleSpec sample_spec_;
+
+    audio::SampleSpec frame_spec_;
+    audio::SampleSpec out_spec_;
 
     bool paused_;
 
diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp
index 8164626c7..2269d151f 100644
--- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp
+++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp
@@ -7,6 +7,7 @@
  */
 
 #include "roc_sndio/sox_source.h"
+#include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
 #include "roc_sndio/backend_map.h"
@@ -15,72 +16,114 @@
 namespace roc {
 namespace sndio {
 
+namespace {
+
+const core::nanoseconds_t DefaultFrameLength = 10 * core::Millisecond;
+
+} // namespace
+
 SoxSource::SoxSource(audio::FrameFactory& frame_factory,
                      core::IArena& arena,
                      const IoConfig& io_config,
-                     DriverType driver_type)
+                     const char* driver,
+                     const char* path)
     : IDevice(arena)
     , ISource(arena)
     , frame_factory_(frame_factory)
-    , driver_type_(driver_type)
-    , driver_name_(arena)
-    , input_name_(arena)
+    , driver_(arena)
+    , path_(arena)
     , buffer_(arena)
     , buffer_size_(0)
     , input_(NULL)
-    , eof_(false)
     , paused_(false)
     , init_status_(status::NoStatus) {
     BackendMap::instance();
 
     if (io_config.latency != 0) {
-        roc_log(LogError, "sox source: setting io latency not supported by sox backend");
+        roc_log(LogError,
+                "sox source: setting io latency not implemented for sox backend");
         init_status_ = status::StatusBadConfig;
         return;
     }
 
-    sample_spec_ = io_config.sample_spec;
+    if (io_config.sample_spec.has_format()
+        && io_config.sample_spec.format() != audio::Format_Pcm) {
+        roc_log(LogError,
+                "sox source: invalid io encoding:"
+                " <format> '%s' not supported by backend: spec=%s",
+                io_config.sample_spec.format_name(),
+                audio::sample_spec_to_str(io_config.sample_spec).c_str());
+        init_status_ = status::StatusBadConfig;
+        return;
+    }
 
-    if (driver_type_ == DriverType_File) {
-        if (!sample_spec_.is_empty()) {
-            roc_log(LogError, "sox source: setting io encoding for files not supported");
+    if (io_config.sample_spec.has_subformat()) {
+        if (io_config.sample_spec.pcm_subformat() == audio::PcmSubformat_Invalid) {
+            roc_log(LogError,
+                    "sox source: invalid io encoding:"
+                    " <subformat> '%s' not supported by backend: spec=%s",
+                    io_config.sample_spec.subformat_name(),
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
             init_status_ = status::StatusBadConfig;
             return;
         }
-    } else {
-        sample_spec_.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                                  audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo,
-                                  0);
 
-        if (!sample_spec_.is_raw()) {
-            roc_log(LogError, "sox sink: sample format can be only \"-\" or \"%s\"",
-                    audio::pcm_format_to_str(audio::Sample_RawFormat));
+        const audio::PcmTraits subfmt =
+            audio::pcm_subformat_traits(io_config.sample_spec.pcm_subformat());
+
+        if (!subfmt.has_flags(audio::Pcm_IsInteger | audio::Pcm_IsSigned)) {
+            roc_log(LogError,
+                    "sox source: invalid io encoding:"
+                    " <subformat> must be signed integer (like s16): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
+
+        if (!subfmt.has_flags(audio::Pcm_IsPacked | audio::Pcm_IsAligned)) {
+            roc_log(LogError,
+                    "sox source: invalid io encoding:"
+                    " <subformat> must be packed (like s24, not s24_4) and byte-aligned"
+                    " (like s16, not s18): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
+
+        if (io_config.sample_spec.pcm_subformat() != subfmt.default_variant) {
+            roc_log(LogError,
+                    "sox source: invalid io encoding:"
+                    " <subformat> must be default-endian (like s16, not s16_le): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
             init_status_ = status::StatusBadConfig;
             return;
         }
     }
 
-    frame_length_ = io_config.frame_length;
+    in_spec_ = io_config.sample_spec;
+    if (!in_spec_.has_format()) {
+        in_spec_.set_format(audio::Format_Pcm);
+        in_spec_.set_pcm_subformat(audio::PcmSubformat_SInt16);
+    }
 
+    frame_length_ = io_config.frame_length;
     if (frame_length_ == 0) {
-        roc_log(LogError, "sox source: frame length is zero");
-        init_status_ = status::StatusBadConfig;
-        return;
+        frame_length_ = DefaultFrameLength;
     }
 
-    {
-        audio::SampleSpec spec = sample_spec_;
-        spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                          audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo, 44100);
+    roc_log(LogDebug, "sox source: opening: driver=%s path=%s", driver, path);
 
-        sox_get_globals()->bufsiz =
-            spec.ns_2_samples_overall(frame_length_) * sizeof(sox_sample_t);
+    if ((init_status_ = init_names_(driver, path)) != status::StatusOK) {
+        return;
     }
 
-    memset(&in_signal_, 0, sizeof(in_signal_));
-    in_signal_.rate = (sox_rate_t)sample_spec_.sample_rate();
-    in_signal_.channels = (unsigned)sample_spec_.num_channels();
-    in_signal_.precision = SOX_SAMPLE_PRECISION;
+    if ((init_status_ = open_()) != status::StatusOK) {
+        return;
+    }
+
+    if ((init_status_ = init_buffer_()) != status::StatusOK) {
+        return;
+    }
 
     init_status_ = status::StatusOK;
 }
@@ -97,30 +140,6 @@ status::StatusCode SoxSource::init_status() const {
     return init_status_;
 }
 
-status::StatusCode SoxSource::open(const char* driver, const char* path) {
-    roc_log(LogInfo, "sox source: opening: driver=%s path=%s", driver, path);
-
-    if (buffer_.size() != 0 || input_) {
-        roc_panic("sox source: can't call open() more than once");
-    }
-
-    status::StatusCode code = status::NoStatus;
-
-    if ((code = init_names_(driver, path)) != status::StatusOK) {
-        return code;
-    }
-
-    if ((code = open_()) != status::StatusOK) {
-        return code;
-    }
-
-    if ((code = init_buffer_()) != status::StatusOK) {
-        return code;
-    }
-
-    return status::StatusOK;
-}
-
 DeviceType SoxSource::type() const {
     return DeviceType_Source;
 }
@@ -134,15 +153,15 @@ ISource* SoxSource::to_source() {
 }
 
 audio::SampleSpec SoxSource::sample_spec() const {
-    if (!input_ && !paused_) {
-        roc_panic("sox source: not opened");
-    }
+    return frame_spec_;
+}
 
-    return sample_spec_;
+core::nanoseconds_t SoxSource::frame_length() const {
+    return frame_length_;
 }
 
 bool SoxSource::has_state() const {
-    return driver_type_ == DriverType_Device;
+    return true;
 }
 
 DeviceState SoxSource::state() const {
@@ -162,14 +181,12 @@ status::StatusCode SoxSource::pause() {
         roc_panic("sox source: not opened");
     }
 
-    roc_log(LogDebug, "sox source: pausing: driver=%s input=%s", driver_name_.c_str(),
-            input_name_.c_str());
+    roc_log(LogDebug, "sox source: pausing: driver=%s path=%s", driver_.c_str(),
+            path_.c_str());
 
-    if (driver_type_ == DriverType_Device) {
-        const status::StatusCode close_code = close_();
-        if (close_code != status::StatusOK) {
-            return close_code;
-        }
+    const status::StatusCode close_code = close_();
+    if (close_code != status::StatusOK) {
+        return close_code;
     }
 
     paused_ = true;
@@ -182,8 +199,8 @@ status::StatusCode SoxSource::resume() {
         return status::StatusOK;
     }
 
-    roc_log(LogDebug, "sox source: resuming: driver=%s input=%s", driver_name_.c_str(),
-            input_name_.c_str());
+    roc_log(LogDebug, "sox source: resuming: driver=%s path=%s", driver_.c_str(),
+            path_.c_str());
 
     if (!input_) {
         const status::StatusCode code = open_();
@@ -202,36 +219,26 @@ bool SoxSource::has_latency() const {
 }
 
 bool SoxSource::has_clock() const {
-    return driver_type_ == DriverType_Device;
+    return true;
 }
 
 status::StatusCode SoxSource::rewind() {
-    roc_log(LogDebug, "sox source: rewinding: driver=%s input=%s", driver_name_.c_str(),
-            input_name_.c_str());
-
-    if (driver_type_ == DriverType_File && !eof_) {
-        const status::StatusCode code = seek_(0);
-        if (code != status::StatusOK) {
-            return code;
-        }
-    } else {
-        sample_spec_.clear();
+    roc_log(LogDebug, "sox source: rewinding: driver=%s path=%s", driver_.c_str(),
+            path_.c_str());
 
-        if (input_) {
-            const status::StatusCode close_code = close_();
-            if (close_code != status::StatusOK) {
-                return close_code;
-            }
+    if (input_) {
+        const status::StatusCode close_code = close_();
+        if (close_code != status::StatusOK) {
+            return close_code;
         }
+    }
 
-        const status::StatusCode code = open_();
-        if (code != status::StatusOK) {
-            return code;
-        }
+    const status::StatusCode code = open_();
+    if (code != status::StatusOK) {
+        return code;
     }
 
     paused_ = false;
-    eof_ = false;
 
     return status::StatusOK;
 }
@@ -244,15 +251,15 @@ status::StatusCode SoxSource::read(audio::Frame& frame,
                                    packet::stream_timestamp_t duration,
                                    audio::FrameReadMode mode) {
     if (!input_ && !paused_) {
-        roc_panic("sox source: read: non-open input file or device");
+        roc_panic("sox source: read: non-open input device");
     }
 
-    if (paused_ || eof_) {
+    if (paused_) {
         return status::StatusFinish;
     }
 
     if (!frame_factory_.reallocate_frame(
-            frame, sample_spec_.stream_timestamp_2_bytes(duration))) {
+            frame, frame_spec_.stream_timestamp_2_bytes(duration))) {
         return status::StatusNoMem;
     }
 
@@ -278,7 +285,6 @@ status::StatusCode SoxSource::read(audio::Frame& frame,
         n_samples = sox_read(input_, buffer_data, n_samples);
         if (n_samples == 0) {
             roc_log(LogDebug, "sox source: got eof from sox");
-            eof_ = true;
             break;
         }
 
@@ -296,7 +302,7 @@ status::StatusCode SoxSource::read(audio::Frame& frame,
     }
 
     frame.set_num_raw_samples(frame_size);
-    frame.set_duration(frame_size / sample_spec_.num_channels());
+    frame.set_duration(frame_size / frame_spec_.num_channels());
 
     if (frame.duration() < duration) {
         return status::StatusPart;
@@ -315,14 +321,14 @@ void SoxSource::dispose() {
 
 status::StatusCode SoxSource::init_names_(const char* driver, const char* path) {
     if (driver) {
-        if (!driver_name_.assign(driver)) {
+        if (!driver_.assign(driver)) {
             roc_log(LogError, "sox source: can't allocate string");
             return status::StatusNoMem;
         }
     }
 
     if (path) {
-        if (!input_name_.assign(path)) {
+        if (!path_.assign(path)) {
             roc_log(LogError, "sox source: can't allocate string");
             return status::StatusNoMem;
         }
@@ -332,7 +338,7 @@ status::StatusCode SoxSource::init_names_(const char* driver, const char* path)
 }
 
 status::StatusCode SoxSource::init_buffer_() {
-    buffer_size_ = sample_spec_.ns_2_samples_overall(frame_length_);
+    buffer_size_ = in_spec_.ns_2_samples_overall(frame_length_);
     if (buffer_size_ == 0) {
         roc_log(LogError, "sox source: buffer size is zero");
         return status::StatusBadConfig;
@@ -347,18 +353,17 @@ status::StatusCode SoxSource::init_buffer_() {
 }
 
 status::StatusCode SoxSource::open_() {
-    if (input_) {
-        roc_panic("sox source: already opened");
-    }
+    memset(&in_signal_, 0, sizeof(in_signal_));
+    in_signal_.rate = (sox_rate_t)in_spec_.sample_rate();
+    in_signal_.channels = (unsigned)in_spec_.num_channels();
+    in_signal_.precision = (unsigned)in_spec_.pcm_bit_width();
 
-    input_ =
-        sox_open_read(input_name_.is_empty() ? NULL : input_name_.c_str(), &in_signal_,
-                      NULL, driver_name_.is_empty() ? NULL : driver_name_.c_str());
+    input_ = sox_open_read(path_.is_empty() ? NULL : path_.c_str(), &in_signal_, NULL,
+                           driver_.is_empty() ? NULL : driver_.c_str());
     if (!input_) {
-        roc_log(LogInfo, "sox source: can't open: driver=%s input=%s",
-                driver_name_.c_str(), input_name_.c_str());
-        return driver_type_ == DriverType_Device ? status::StatusErrDevice
-                                                 : status::StatusErrFile;
+        roc_log(LogInfo, "sox source: can't open: driver=%s path=%s", driver_.c_str(),
+                path_.c_str());
+        return status::StatusErrDevice;
     }
 
     const unsigned long requested_rate = (unsigned long)in_signal_.rate;
@@ -367,11 +372,10 @@ status::StatusCode SoxSource::open_() {
     if (requested_rate != 0 && requested_rate != actual_rate) {
         roc_log(LogError,
                 "sox source:"
-                " can't open input file or device with the requested sample rate:"
+                " can't open input device with the requested sample rate:"
                 " required_by_input=%lu requested_by_user=%lu",
                 actual_rate, requested_rate);
-        return driver_type_ == DriverType_Device ? status::StatusErrDevice
-                                                 : status::StatusErrFile;
+        return status::StatusErrDevice;
     }
 
     const unsigned long requested_chans = (unsigned long)in_signal_.channels;
@@ -380,38 +384,35 @@ status::StatusCode SoxSource::open_() {
     if (requested_chans != 0 && requested_chans != actual_chans) {
         roc_log(LogError,
                 "sox source:"
-                " can't open input file or device with the requested channel count:"
+                " can't open input device with the requested channel count:"
                 " required_by_input=%lu requested_by_user=%lu",
                 actual_chans, requested_chans);
-        return driver_type_ == DriverType_Device ? status::StatusErrDevice
-                                                 : status::StatusErrFile;
+        return status::StatusErrDevice;
     }
 
-    sample_spec_.set_sample_format(audio::SampleFormat_Pcm);
-    sample_spec_.set_pcm_format(audio::Sample_RawFormat);
-    sample_spec_.set_sample_rate(actual_rate);
-    sample_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
-    sample_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
-    sample_spec_.channel_set().set_count(actual_chans);
+    const unsigned long requested_bits = (unsigned long)in_signal_.precision;
+    const unsigned long actual_bits = (unsigned long)input_->signal.precision;
 
-    roc_log(LogInfo,
-            "sox source: opened input:"
-            " bits=%lu rate=%lu req_rate=%lu chans=%lu req_chans=%lu is_file=%d",
-            (unsigned long)input_->encoding.bits_per_sample, actual_rate, requested_rate,
-            actual_chans, requested_chans, (int)(driver_type_ == DriverType_File));
+    if (requested_bits != 0 && requested_bits != actual_bits) {
+        roc_log(LogError,
+                "sox source:"
+                " can't open input device with the requested subformat:"
+                " supported=s%lu requested=s%lu",
+                actual_bits, requested_bits);
+        return status::StatusErrDevice;
+    }
 
-    return status::StatusOK;
-}
+    in_spec_.set_sample_rate(actual_rate);
+    in_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
+    in_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
+    in_spec_.channel_set().set_count(actual_chans);
 
-status::StatusCode SoxSource::seek_(uint64_t offset) {
-    roc_log(LogDebug, "sox source: resetting position to %lu", (unsigned long)offset);
+    frame_spec_ = in_spec_;
+    frame_spec_.set_format(audio::Format_Pcm);
+    frame_spec_.set_pcm_subformat(audio::PcmSubformat_Raw);
 
-    const int err = sox_seek(input_, offset, SOX_SEEK_SET);
-    if (err != SOX_SUCCESS) {
-        roc_log(LogError, "sox source: can't reset position to %lu: %s",
-                (unsigned long)offset, sox_strerror(err));
-        return status::StatusErrFile;
-    }
+    roc_log(LogInfo, "sox source: input output %s",
+            audio::sample_spec_to_str(in_spec_).c_str());
 
     return status::StatusOK;
 }
@@ -428,8 +429,7 @@ status::StatusCode SoxSource::close_() {
 
     if (err != SOX_SUCCESS) {
         roc_log(LogError, "sox source: can't close input: %s", sox_strerror(err));
-        return driver_type_ == DriverType_File ? status::StatusErrFile
-                                               : status::StatusErrDevice;
+        return status::StatusErrDevice;
     }
 
     return status::StatusOK;
diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.h b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.h
index c183beb73..7cbf82bce 100644
--- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.h
+++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.h
@@ -29,23 +29,22 @@ namespace sndio {
 
 //! SoX source.
 //! @remarks
-//!  Reads samples from input file or device.
-//!  Supports multiple drivers for different file types and audio systems.
+//!  Reads samples from input device.
+//!  Supports multiple drivers for different audio systems.
+//!  Does not support files.
 class SoxSource : public ISource, private core::NonCopyable<> {
 public:
     //! Initialize.
     SoxSource(audio::FrameFactory& frame_factory,
               core::IArena& arena,
               const IoConfig& io_config,
-              DriverType driver_type);
+              const char* driver,
+              const char* path);
     ~SoxSource();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open sink.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* driver, const char* path);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -58,6 +57,9 @@ class SoxSource : public ISource, private core::NonCopyable<> {
     //! Get sample specification of the source.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the source.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the source supports state updates.
     virtual bool has_state() const;
 
@@ -99,24 +101,23 @@ class SoxSource : public ISource, private core::NonCopyable<> {
     status::StatusCode init_buffer_();
 
     status::StatusCode open_();
-    status::StatusCode seek_(uint64_t offset);
     status::StatusCode close_();
 
     audio::FrameFactory& frame_factory_;
 
-    const DriverType driver_type_;
-    core::StringBuffer driver_name_;
-    core::StringBuffer input_name_;
+    core::StringBuffer driver_;
+    core::StringBuffer path_;
 
     core::Array<sox_sample_t> buffer_;
     size_t buffer_size_;
     core::nanoseconds_t frame_length_;
-    audio::SampleSpec sample_spec_;
+
+    audio::SampleSpec frame_spec_;
+    audio::SampleSpec in_spec_;
 
     sox_format_t* input_;
     sox_signalinfo_t in_signal_;
 
-    bool eof_;
     bool paused_;
 
     status::StatusCode init_status_;
diff --git a/src/internal_modules/roc_sndio/wav_backend.cpp b/src/internal_modules/roc_sndio/wav_backend.cpp
index d33ef3340..cff29aeee 100644
--- a/src/internal_modules/roc_sndio/wav_backend.cpp
+++ b/src/internal_modules/roc_sndio/wav_backend.cpp
@@ -19,60 +19,59 @@
 namespace roc {
 namespace sndio {
 
-namespace {
+WavBackend::WavBackend() {
+}
 
-bool has_suffix(const char* str, const char* suffix) {
-    size_t len_str = strlen(str);
-    size_t len_suffix = strlen(suffix);
-    if (len_suffix > len_str) {
+const char* WavBackend::name() const {
+    return "wav";
+}
+
+bool WavBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& result) {
+    if (!result.push_back(DriverInfo(
+            "file", Driver_File | Driver_SupportsSink | Driver_SupportsSource, this))) {
         return false;
     }
-    return strncmp(str + len_str - len_suffix, suffix, len_suffix) == 0;
+    return true;
 }
 
-} // namespace
-
-WavBackend::WavBackend() {
+bool WavBackend::discover_formats(core::Array<FormatInfo, MaxFormats>& result) {
+    if (!result.push_back(FormatInfo(
+            "file", "wav", Driver_File | Driver_SupportsSink | Driver_SupportsSource,
+            this))) {
+        return false;
+    }
+    return true;
 }
 
-const char* WavBackend::name() const {
-    return "wav";
+bool WavBackend::discover_subformat_groups(core::StringList& result) {
+    // no sub-formats except pcm
+    return true;
 }
 
-void WavBackend::discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list) {
-    if (!driver_list.push_back(
-            DriverInfo("wav", DriverType_File,
-                       DriverFlag_SupportsSink | DriverFlag_SupportsSource, this))) {
-        roc_panic("wav backend: can't add driver");
-    }
+bool WavBackend::discover_subformats(const char* group, core::StringList& result) {
+    // no sub-formats except pcm
+    return true;
 }
 
 status::StatusCode WavBackend::open_device(DeviceType device_type,
-                                           DriverType driver_type,
                                            const char* driver,
                                            const char* path,
                                            const IoConfig& io_config,
                                            audio::FrameFactory& frame_factory,
                                            core::IArena& arena,
                                            IDevice** result) {
-    if (driver_type != DriverType_File) {
-        return status::StatusNoDriver;
-    }
+    roc_panic_if(!driver);
+    roc_panic_if(!path);
 
-    if (driver) {
-        if (strcmp(driver, "wav") != 0) {
-            return status::StatusNoDriver;
-        }
-    } else {
-        if (!has_suffix(path, ".wav")) {
-            return status::StatusNoDriver;
-        }
+    if (strcmp(driver, "file") != 0) {
+        // Not file://, go to next backend.
+        return status::StatusNoDriver;
     }
 
     switch (device_type) {
     case DeviceType_Sink: {
         core::ScopedPtr<WavSink> sink(new (arena)
-                                          WavSink(frame_factory, arena, io_config));
+                                          WavSink(frame_factory, arena, io_config, path));
 
         if (!sink) {
             roc_log(LogDebug, "wav backend: can't allocate sink: path=%s", path);
@@ -80,16 +79,9 @@ status::StatusCode WavBackend::open_device(DeviceType device_type,
         }
 
         if (sink->init_status() != status::StatusOK) {
-            roc_log(LogDebug, "wav backend: can't initialize sink: path=%s status=%s",
-                    path, status::code_to_str(sink->init_status()));
-            return sink->init_status();
-        }
-
-        const status::StatusCode code = sink->open(path);
-        if (code != status::StatusOK) {
             roc_log(LogDebug, "wav backend: can't open sink: path=%s status=%s", path,
-                    status::code_to_str(code));
-            return code;
+                    status::code_to_str(sink->init_status()));
+            return sink->init_status();
         }
 
         *result = sink.hijack();
@@ -97,8 +89,8 @@ status::StatusCode WavBackend::open_device(DeviceType device_type,
     } break;
 
     case DeviceType_Source: {
-        core::ScopedPtr<WavSource> source(new (arena)
-                                              WavSource(frame_factory, arena, io_config));
+        core::ScopedPtr<WavSource> source(
+            new (arena) WavSource(frame_factory, arena, io_config, path));
 
         if (!source) {
             roc_log(LogDebug, "wav backend: can't allocate source: path=%s", path);
@@ -106,24 +98,14 @@ status::StatusCode WavBackend::open_device(DeviceType device_type,
         }
 
         if (source->init_status() != status::StatusOK) {
-            roc_log(LogDebug, "wav backend: can't initialize source: path=%s status=%s",
-                    path, status::code_to_str(source->init_status()));
-            return source->init_status();
-        }
-
-        const status::StatusCode code = source->open(path);
-        if (code != status::StatusOK) {
             roc_log(LogDebug, "wav backend: can't open source: path=%s status=%s", path,
-                    status::code_to_str(code));
-            return code;
+                    status::code_to_str(source->init_status()));
+            return source->init_status();
         }
 
         *result = source.hijack();
         return status::StatusOK;
     } break;
-
-    default:
-        break;
     }
 
     roc_panic("wav backend: invalid device type");
diff --git a/src/internal_modules/roc_sndio/wav_backend.h b/src/internal_modules/roc_sndio/wav_backend.h
index 72cae1a24..15f671dde 100644
--- a/src/internal_modules/roc_sndio/wav_backend.h
+++ b/src/internal_modules/roc_sndio/wav_backend.h
@@ -27,12 +27,23 @@ class WavBackend : public IBackend, core::NonCopyable<> {
     virtual const char* name() const;
 
     //! Append supported drivers to the list.
-    virtual void discover_drivers(core::Array<DriverInfo, MaxDrivers>& driver_list);
+    virtual ROC_ATTR_NODISCARD bool
+    discover_drivers(core::Array<DriverInfo, MaxDrivers>& result);
+
+    //! Append supported formats to the list.
+    virtual ROC_ATTR_NODISCARD bool
+    discover_formats(core::Array<FormatInfo, MaxFormats>& result);
+
+    //! Append supported groups of sub-formats to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformat_groups(core::StringList& result);
+
+    //! Append supported sub-formats of a group to the list.
+    virtual ROC_ATTR_NODISCARD bool discover_subformats(const char* group,
+                                                        core::StringList& result);
 
     //! Create and open a sink or source.
     virtual ROC_ATTR_NODISCARD status::StatusCode
     open_device(DeviceType device_type,
-                DriverType driver_type,
                 const char* driver,
                 const char* path,
                 const IoConfig& io_config,
diff --git a/src/internal_modules/roc_sndio/wav_sink.cpp b/src/internal_modules/roc_sndio/wav_sink.cpp
index 478c93e83..793e6cc86 100644
--- a/src/internal_modules/roc_sndio/wav_sink.cpp
+++ b/src/internal_modules/roc_sndio/wav_sink.cpp
@@ -7,8 +7,8 @@
  */
 
 #include "roc_sndio/wav_sink.h"
-#include "roc_audio/pcm_format.h"
-#include "roc_audio/sample_format.h"
+#include "roc_audio/format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/endian_ops.h"
 #include "roc_core/log.h"
@@ -18,71 +18,118 @@
 namespace roc {
 namespace sndio {
 
+namespace {
+
+bool has_extension(const char* path, const char* ext) {
+    size_t path_len = strlen(path);
+    size_t ext_len = strlen(ext);
+    if (ext_len > path_len) {
+        return false;
+    }
+    return strncmp(path + path_len - ext_len, ext, ext_len) == 0;
+}
+
+} // namespace
+
 WavSink::WavSink(audio::FrameFactory& frame_factory,
                  core::IArena& arena,
-                 const IoConfig& io_config)
+                 const IoConfig& io_config,
+                 const char* path)
     : IDevice(arena)
     , ISink(arena)
     , output_file_(NULL)
     , is_first_(true)
     , init_status_(status::NoStatus) {
-    if (io_config.latency != 0) {
-        roc_log(LogError, "wav sink: setting io latency not supported by backend");
-        init_status_ = status::StatusBadConfig;
-        return;
+    if (io_config.sample_spec.has_format()) {
+        if (io_config.sample_spec.format() != audio::Format_Wav) {
+            roc_log(LogDebug,
+                    "wav sink: requested format '%s' not supported by backend: spec=%s",
+                    io_config.sample_spec.format_name(),
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            // Not a wav file, go to next backend.
+            init_status_ = status::StatusNoFormat;
+            return;
+        }
+    } else {
+        if (!has_extension(path, ".wav")) {
+            roc_log(
+                LogDebug,
+                "wav sink: requested file extension not supported by backend: path=%s",
+                path);
+            // Not a wav file, go to next backend.
+            init_status_ = status::StatusNoFormat;
+            return;
+        }
     }
 
-    sample_spec_ = io_config.sample_spec;
-
-    sample_spec_.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                              audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo,
-                              44100);
+    if (io_config.sample_spec.has_subformat()) {
+        if (io_config.sample_spec.pcm_subformat() == audio::PcmSubformat_Invalid) {
+            roc_log(LogDebug,
+                    "wav sink: invalid io encoding:"
+                    " <subformat> must be pcm (like s16 or f32): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
 
-    if (!sample_spec_.is_pcm()) {
-        roc_log(LogError, "wav sink: unsupported format: must be pcm: spec=%s",
-                audio::sample_spec_to_str(sample_spec_).c_str());
-        init_status_ = status::StatusBadConfig;
-        return;
-    }
+        const audio::PcmTraits subfmt =
+            audio::pcm_subformat_traits(io_config.sample_spec.pcm_subformat());
+
+        if (!subfmt.has_flags(audio::Pcm_IsSigned)) {
+            roc_log(LogError,
+                    "wav sink: invalid io encoding:"
+                    " <subformat> must be float (like f32) or signed integer (like s16): "
+                    "spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
 
-    const audio::PcmTraits fmt = audio::pcm_format_traits(sample_spec_.pcm_format());
+        if (!subfmt.has_flags(audio::Pcm_IsPacked | audio::Pcm_IsAligned)) {
+            roc_log(LogError,
+                    "wav sink: invalid io encoding:"
+                    " <subformat> must be packed (like s24, not s24_4) and byte-aligned"
+                    " (like s16, not s18): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
 
-    if (!fmt.has_flags(audio::Pcm_IsSigned)) {
-        roc_log(LogError, "wav sink: unsupported format: must be signed: spec=%s",
-                audio::sample_spec_to_str(sample_spec_).c_str());
-        init_status_ = status::StatusBadConfig;
-        return;
+        if (io_config.sample_spec.pcm_subformat() != subfmt.default_variant
+            && io_config.sample_spec.pcm_subformat() != subfmt.le_variant) {
+            roc_log(LogError,
+                    "wav sink: invalid io encoding:"
+                    " <subformat> must be default-endian (like s16) or little-endian"
+                    " (like s16_le): spec=%s",
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            init_status_ = status::StatusBadConfig;
+            return;
+        }
     }
 
-    if (!fmt.has_flags(audio::Pcm_IsPacked | audio::Pcm_IsAligned)) {
-        roc_log(LogError,
-                "wav sink: unsupported format: must be packed and byte-aligned: spec=%s",
-                audio::sample_spec_to_str(sample_spec_).c_str());
-        init_status_ = status::StatusBadConfig;
-        return;
-    }
+    file_spec_ = io_config.sample_spec;
+    file_spec_.use_defaults(audio::Format_Wav, audio::PcmSubformat_Raw,
+                            audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
+                            audio::ChanMask_Surround_Stereo, 44100);
 
-    // WAV format is always little-endian.
-    if (sample_spec_.pcm_format() != fmt.default_variant
-        && sample_spec_.pcm_format() != fmt.le_variant) {
-        roc_log(LogError,
-                "wav sink: sample format must be default-endian (like s16) or"
-                " little-endian (like s16_le): spec=%s",
-                audio::sample_spec_to_str(sample_spec_).c_str());
-        init_status_ = status::StatusBadConfig;
-        return;
-    }
+    const audio::PcmTraits subfmt =
+        audio::pcm_subformat_traits(file_spec_.pcm_subformat());
 
-    if (sample_spec_.pcm_format() == fmt.default_variant) {
-        sample_spec_.set_pcm_format(fmt.le_variant);
+    frame_spec_ = file_spec_;
+    frame_spec_.set_format(audio::Format_Pcm);
+    if (frame_spec_.pcm_subformat() == subfmt.default_variant) {
+        frame_spec_.set_pcm_subformat(subfmt.le_variant);
     }
 
     const uint16_t fmt_code =
-        fmt.has_flags(audio::Pcm_IsInteger) ? WAV_FORMAT_PCM : WAV_FORMAT_IEEE_FLOAT;
+        subfmt.has_flags(audio::Pcm_IsInteger) ? WAV_FORMAT_PCM : WAV_FORMAT_IEEE_FLOAT;
 
-    header_.reset(new (header_)
-                      WavHeader(fmt_code, fmt.bit_width, sample_spec_.sample_rate(),
-                                sample_spec_.num_channels()));
+    header_.reset(new (header_) WavHeader(
+        fmt_code, subfmt.bit_width, file_spec_.sample_rate(), file_spec_.num_channels()));
+
+    if ((init_status_ = open_(path)) != status::StatusOK) {
+        return;
+    }
 
     init_status_ = status::StatusOK;
 }
@@ -98,10 +145,6 @@ status::StatusCode WavSink::init_status() const {
     return init_status_;
 }
 
-status::StatusCode WavSink::open(const char* path) {
-    return open_(path);
-}
-
 DeviceType WavSink::type() const {
     return DeviceType_Sink;
 }
@@ -119,7 +162,11 @@ audio::SampleSpec WavSink::sample_spec() const {
         roc_panic("wav sink: not opened");
     }
 
-    return sample_spec_;
+    return frame_spec_;
+}
+
+core::nanoseconds_t WavSink::frame_length() const {
+    return 0;
 }
 
 bool WavSink::has_state() const {
@@ -139,6 +186,8 @@ status::StatusCode WavSink::write(audio::Frame& frame) {
         roc_panic("wav sink: not opened");
     }
 
+    frame_spec_.validate_frame(frame);
+
     if (is_first_) {
         const WavHeader::WavHeaderData& wav_header = header_->update_and_get_header(0);
         if (fwrite(&wav_header, sizeof(wav_header), 1, output_file_) != 1) {
@@ -204,24 +253,20 @@ void WavSink::dispose() {
 }
 
 status::StatusCode WavSink::open_(const char* path) {
-    if (output_file_) {
-        roc_panic("wav sink: already opened");
-    }
+    roc_log(LogDebug, "wav sink: opening: path=%s", path);
 
-    output_file_ = fopen(path, "w");
-
-    if (!output_file_) {
-        roc_log(LogDebug, "wav sink: can't open output file: %s",
-                core::errno_to_str(errno).c_str());
-        return status::StatusErrFile;
+    if (strcmp(path, "-") == 0) {
+        output_file_ = stdout;
+    } else {
+        if (!(output_file_ = fopen(path, "wb"))) {
+            roc_log(LogDebug, "wav sink: can't open output file: %s",
+                    core::errno_to_str(errno).c_str());
+            return status::StatusErrFile;
+        }
     }
 
-    roc_log(LogInfo,
-            "wav sink: opened output file:"
-            " path=%s out_bits=%lu out_rate=%lu out_ch=%lu",
-            path, (unsigned long)header_->bits_per_sample(),
-            (unsigned long)header_->sample_rate(),
-            (unsigned long)header_->num_channels());
+    roc_log(LogInfo, "wav sink: opened output file: %s",
+            audio::sample_spec_to_str(file_spec_).c_str());
 
     return status::StatusOK;
 }
@@ -233,13 +278,17 @@ status::StatusCode WavSink::close_() {
 
     roc_log(LogDebug, "wav sink: closing output file");
 
-    const int err = fclose(output_file_);
-    output_file_ = NULL;
+    if (output_file_ == stdout) {
+        output_file_ = NULL;
+    } else {
+        const int err = fclose(output_file_);
+        output_file_ = NULL;
 
-    if (err != 0) {
-        roc_log(LogError, "wav sink: can't properly close output file: %s",
-                core::errno_to_str(errno).c_str());
-        return status::StatusErrFile;
+        if (err != 0) {
+            roc_log(LogError, "wav sink: can't properly close output file: %s",
+                    core::errno_to_str(errno).c_str());
+            return status::StatusErrFile;
+        }
     }
 
     return status::StatusOK;
diff --git a/src/internal_modules/roc_sndio/wav_sink.h b/src/internal_modules/roc_sndio/wav_sink.h
index d9f1b1882..dec0b5e67 100644
--- a/src/internal_modules/roc_sndio/wav_sink.h
+++ b/src/internal_modules/roc_sndio/wav_sink.h
@@ -30,15 +30,13 @@ class WavSink : public ISink, public core::NonCopyable<> {
     //! Initialize.
     WavSink(audio::FrameFactory& frame_factory,
             core::IArena& arena,
-            const IoConfig& io_config);
+            const IoConfig& io_config,
+            const char* path);
     ~WavSink();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open sink.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* device);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -51,6 +49,9 @@ class WavSink : public ISink, public core::NonCopyable<> {
     //! Get sample specification of the sink.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the sink.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the sink supports state updates.
     virtual bool has_state() const;
 
@@ -76,7 +77,8 @@ class WavSink : public ISink, public core::NonCopyable<> {
     status::StatusCode open_(const char* path);
     status::StatusCode close_();
 
-    audio::SampleSpec sample_spec_;
+    audio::SampleSpec frame_spec_;
+    audio::SampleSpec file_spec_;
 
     FILE* output_file_;
     core::Optional<WavHeader> header_;
diff --git a/src/internal_modules/roc_sndio/wav_source.cpp b/src/internal_modules/roc_sndio/wav_source.cpp
index f4dba94f4..2724e59fc 100644
--- a/src/internal_modules/roc_sndio/wav_source.cpp
+++ b/src/internal_modules/roc_sndio/wav_source.cpp
@@ -7,6 +7,7 @@
  */
 
 #include "roc_sndio/wav_source.h"
+#include "roc_audio/sample_spec_to_str.h"
 #include "roc_core/log.h"
 #include "roc_core/panic.h"
 #include "roc_status/code_to_str.h"
@@ -14,24 +15,71 @@
 namespace roc {
 namespace sndio {
 
+namespace {
+
+bool has_extension(const char* path, const char* ext) {
+    size_t path_len = strlen(path);
+    size_t ext_len = strlen(ext);
+    if (ext_len > path_len) {
+        return false;
+    }
+    return strncmp(path + path_len - ext_len, ext, ext_len) == 0;
+}
+
+size_t file_read(void* file, void* buf, size_t bufsz) {
+    return fread(buf, 1, bufsz, (FILE*)file);
+}
+
+drwav_bool32 file_seek(void* file, int offset, drwav_seek_origin origin) {
+    return fseek((FILE*)file, offset,
+                 origin == drwav_seek_origin_current ? SEEK_CUR : SEEK_SET)
+        == 0;
+}
+
+} // namespace
+
 WavSource::WavSource(audio::FrameFactory& frame_factory,
                      core::IArena& arena,
-                     const IoConfig& io_config)
+                     const IoConfig& io_config,
+                     const char* path)
     : IDevice(arena)
     , ISource(arena)
     , frame_factory_(frame_factory)
-    , file_opened_(false)
+    , input_file_(NULL)
     , eof_(false)
     , init_status_(status::NoStatus) {
-    if (io_config.latency != 0) {
-        roc_log(LogError, "wav source: setting io latency not supported by backend");
+    if (io_config.sample_spec.has_format()) {
+        if (io_config.sample_spec.format() != audio::Format_Wav) {
+            roc_log(LogDebug,
+                    "wav source: requested format '%s' not supported by backend: spec=%s",
+                    io_config.sample_spec.format_name(),
+                    audio::sample_spec_to_str(io_config.sample_spec).c_str());
+            // Not a wav file, go to next backend.
+            init_status_ = status::StatusNoFormat;
+            return;
+        }
+    } else {
+        if (!has_extension(path, ".wav")) {
+            roc_log(
+                LogDebug,
+                "wav source: requested file extension not supported by backend: path=%s",
+                path);
+            // Not a wav file, go to next backend.
+            init_status_ = status::StatusNoFormat;
+            return;
+        }
+    }
+
+    if (io_config.sample_spec.has_subformat() || io_config.sample_spec.has_sample_rate()
+        || io_config.sample_spec.has_channel_set()) {
+        roc_log(LogError,
+                "wav source: invalid io encoding: <subformat>, <rate> and <channels>"
+                " not allowed for input file when <format> is 'wav', set them to \"-\"");
         init_status_ = status::StatusBadConfig;
         return;
     }
 
-    if (!io_config.sample_spec.is_empty()) {
-        roc_log(LogError, "wav source: setting io encoding not supported by backend");
-        init_status_ = status::StatusBadConfig;
+    if ((init_status_ = open_(path)) != status::StatusOK) {
         return;
     }
 
@@ -50,10 +98,6 @@ status::StatusCode WavSource::init_status() const {
     return init_status_;
 }
 
-status::StatusCode WavSource::open(const char* path) {
-    return open_(path);
-}
-
 DeviceType WavSource::type() const {
     return DeviceType_Source;
 }
@@ -67,13 +111,17 @@ ISource* WavSource::to_source() {
 }
 
 audio::SampleSpec WavSource::sample_spec() const {
-    if (!file_opened_) {
+    if (!input_file_) {
         roc_panic("wav source: not opened");
     }
 
     return sample_spec_;
 }
 
+core::nanoseconds_t WavSource::frame_length() const {
+    return 0;
+}
+
 bool WavSource::has_state() const {
     return false;
 }
@@ -89,11 +137,11 @@ bool WavSource::has_clock() const {
 status::StatusCode WavSource::rewind() {
     roc_log(LogDebug, "wav source: rewinding");
 
-    if (!file_opened_) {
+    if (!input_file_) {
         roc_panic("wav source: not opened");
     }
 
-    if (!drwav_seek_to_pcm_frame(&wav_, 0)) {
+    if (!drwav_seek_to_pcm_frame(&wav_decoder_, 0)) {
         roc_log(LogError, "wav source: seek failed");
         return status::StatusErrFile;
     }
@@ -110,7 +158,7 @@ void WavSource::reclock(core::nanoseconds_t timestamp) {
 status::StatusCode WavSource::read(audio::Frame& frame,
                                    packet::stream_timestamp_t duration,
                                    audio::FrameReadMode mode) {
-    if (!file_opened_) {
+    if (!input_file_) {
         roc_panic("wav source: not opened");
     }
 
@@ -132,9 +180,15 @@ status::StatusCode WavSource::read(audio::Frame& frame,
     while (frame_left != 0) {
         size_t n_samples = frame_left;
 
-        n_samples =
-            drwav_read_pcm_frames_f32(&wav_, n_samples / wav_.channels, frame_data)
-            * wav_.channels;
+        n_samples = drwav_read_pcm_frames_f32(
+                        &wav_decoder_, n_samples / wav_decoder_.channels, frame_data)
+            * wav_decoder_.channels;
+
+        if (ferror(input_file_)) {
+            roc_log(LogError, "wav source: can't read input file: %s",
+                    core::errno_to_str(errno).c_str());
+            return status::StatusErrFile;
+        }
 
         if (n_samples == 0) {
             roc_log(LogDebug, "wav source: got eof from input file");
@@ -152,7 +206,8 @@ status::StatusCode WavSource::read(audio::Frame& frame,
     }
 
     frame.set_num_raw_samples(frame_size);
-    frame.set_duration(frame_size / sample_spec_.num_channels());
+    frame.set_duration(
+        packet::stream_timestamp_t(frame_size / sample_spec_.num_channels()));
 
     if (frame.duration() < duration) {
         return status::StatusPart;
@@ -170,48 +225,65 @@ void WavSource::dispose() {
 }
 
 status::StatusCode WavSource::open_(const char* path) {
-    if (file_opened_) {
-        roc_panic("wav source: already opened");
+    roc_log(LogDebug, "wav source: opening: path=%s", path);
+
+    if (strcmp(path, "-") == 0) {
+        input_file_ = stdin;
+    } else {
+        if (!(input_file_ = fopen(path, "rb"))) {
+            roc_log(LogError, "wav source: can't open input file: %s",
+                    core::errno_to_str(errno).c_str());
+            return status::StatusErrFile;
+        }
     }
 
-    if (!drwav_init_file(&wav_, path, NULL)) {
-        roc_log(LogDebug, "wav sink: can't open input file: %s",
-                core::errno_to_str(errno).c_str());
-        return status::StatusErrFile;
+    if (!drwav_init(&wav_decoder_, &file_read, &file_seek, input_file_, NULL)) {
+        roc_log(LogDebug, "wav source: can't recognize input file format");
+        if (input_file_ != stdin) {
+            fclose(input_file_);
+        }
+        input_file_ = NULL;
+        return status::StatusNoFormat;
     }
 
-    roc_log(LogInfo,
-            "wav source: opened input file:"
-            " path=%s in_bits=%lu in_rate=%lu in_ch=%lu",
-            path, (unsigned long)wav_.bitsPerSample, (unsigned long)wav_.sampleRate,
-            (unsigned long)wav_.channels);
-
-    sample_spec_.set_sample_rate((size_t)wav_.sampleRate);
-    sample_spec_.set_sample_format(audio::SampleFormat_Pcm);
-    sample_spec_.set_pcm_format(audio::Sample_RawFormat);
+    sample_spec_.set_format(audio::Format_Pcm);
+    sample_spec_.set_pcm_subformat(audio::PcmSubformat_Raw);
+    sample_spec_.set_sample_rate((size_t)wav_decoder_.sampleRate);
     sample_spec_.channel_set().set_layout(audio::ChanLayout_Surround);
     sample_spec_.channel_set().set_order(audio::ChanOrder_Smpte);
-    sample_spec_.channel_set().set_count((size_t)wav_.channels);
+    sample_spec_.channel_set().set_count((size_t)wav_decoder_.channels);
 
-    file_opened_ = true;
+    roc_log(LogInfo, "wav source: opened input file: %s",
+            audio::sample_spec_to_str(sample_spec_).c_str());
 
     return status::StatusOK;
 }
 
 status::StatusCode WavSource::close_() {
-    if (!file_opened_) {
+    if (!input_file_) {
         return status::StatusOK;
     }
 
-    roc_log(LogInfo, "sndfile source: closing input file");
+    roc_log(LogInfo, "wav source: closing input file");
 
-    file_opened_ = false;
-
-    if (drwav_uninit(&wav_) != DRWAV_SUCCESS) {
+    if (drwav_uninit(&wav_decoder_) != DRWAV_SUCCESS) {
         roc_log(LogError, "wav source: can't properly close input file");
         return status::StatusErrFile;
     }
 
+    if (input_file_ == stdin) {
+        input_file_ = NULL;
+    } else {
+        const int err = fclose(input_file_);
+        input_file_ = NULL;
+
+        if (err != 0) {
+            roc_log(LogError, "wav source: can't properly close input file: %s",
+                    core::errno_to_str(errno).c_str());
+            return status::StatusErrFile;
+        }
+    }
+
     return status::StatusOK;
 }
 
diff --git a/src/internal_modules/roc_sndio/wav_source.h b/src/internal_modules/roc_sndio/wav_source.h
index 10f957dcb..e447aa4c8 100644
--- a/src/internal_modules/roc_sndio/wav_source.h
+++ b/src/internal_modules/roc_sndio/wav_source.h
@@ -31,15 +31,13 @@ class WavSource : public ISource, private core::NonCopyable<> {
     //! Initialize.
     WavSource(audio::FrameFactory& frame_factory,
               core::IArena& arena,
-              const IoConfig& io_config);
+              const IoConfig& io_config,
+              const char* path);
     ~WavSource();
 
     //! Check if the object was successfully constructed.
     status::StatusCode init_status() const;
 
-    //! Open source.
-    ROC_ATTR_NODISCARD status::StatusCode open(const char* device);
-
     //! Get device type.
     virtual DeviceType type() const;
 
@@ -52,6 +50,9 @@ class WavSource : public ISource, private core::NonCopyable<> {
     //! Get sample specification of the source.
     virtual audio::SampleSpec sample_spec() const;
 
+    //! Get recommended frame length of the source.
+    virtual core::nanoseconds_t frame_length() const;
+
     //! Check if the source supports state updates.
     virtual bool has_state() const;
 
@@ -87,8 +88,8 @@ class WavSource : public ISource, private core::NonCopyable<> {
 
     audio::SampleSpec sample_spec_;
 
-    drwav wav_;
-    bool file_opened_;
+    FILE* input_file_;
+    drwav wav_decoder_;
     bool eof_;
 
     status::StatusCode init_status_;
diff --git a/src/internal_modules/roc_status/code_to_str.cpp b/src/internal_modules/roc_status/code_to_str.cpp
index da490582e..46313c0b8 100644
--- a/src/internal_modules/roc_status/code_to_str.cpp
+++ b/src/internal_modules/roc_status/code_to_str.cpp
@@ -34,6 +34,8 @@ const char* code_to_str(StatusCode code) {
         return "NoRoute";
     case StatusNoDriver:
         return "NoDriver";
+    case StatusNoFormat:
+        return "NoFormat";
     case StatusNoPlugin:
         return "NoPlugin";
     case StatusErrDevice:
diff --git a/src/internal_modules/roc_status/status_code.h b/src/internal_modules/roc_status/status_code.h
index 33c3d27b3..af955fc00 100644
--- a/src/internal_modules/roc_status/status_code.h
+++ b/src/internal_modules/roc_status/status_code.h
@@ -86,11 +86,19 @@ enum StatusCode {
     //! @remarks
     //!  Indicates that there is no suitable driver to open sink or source.
     //! @note
+    //!  Example: we're trying to open a pulseaudio device using a backend
+    //!  that supports only alsa devices.
+    StatusNoDriver,
+
+    //! Unsupported format.
+    //! @remarks
+    //!  Indicates that the format or sub-format requested is not supported.
+    //! @note
     //!  Example: we're trying to open an mp3 file using a backend that
     //!  supports only wav files.
-    StatusNoDriver,
+    StatusNoFormat,
 
-    //! No plugin found.
+    //! Unusable or missing plugin.
     //! @remarks
     //!  Indicates that plugin lookup or initialization failed.
     //! @note
diff --git a/src/public_api/examples/basic_receiver_pulseaudio.c b/src/public_api/examples/basic_receiver_pulseaudio.c
index 8160324bb..a529d4a2f 100644
--- a/src/public_api/examples/basic_receiver_pulseaudio.c
+++ b/src/public_api/examples/basic_receiver_pulseaudio.c
@@ -66,8 +66,9 @@ int main() {
     memset(&receiver_config, 0, sizeof(receiver_config));
 
     /* Setup frame format that we want to read from receiver. */
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Use user-provided clock.
diff --git a/src/public_api/examples/basic_receiver_wav_file.c b/src/public_api/examples/basic_receiver_wav_file.c
index 224e91693..b9490c98c 100644
--- a/src/public_api/examples/basic_receiver_wav_file.c
+++ b/src/public_api/examples/basic_receiver_wav_file.c
@@ -113,8 +113,9 @@ int main() {
     memset(&receiver_config, 0, sizeof(receiver_config));
 
     /* Setup frame format that we want to read from receiver. */
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Turn on internal CPU timer.
diff --git a/src/public_api/examples/basic_sender_pulseaudio.c b/src/public_api/examples/basic_sender_pulseaudio.c
index 31845b169..600001773 100644
--- a/src/public_api/examples/basic_sender_pulseaudio.c
+++ b/src/public_api/examples/basic_sender_pulseaudio.c
@@ -67,8 +67,9 @@ int main() {
     memset(&sender_config, 0, sizeof(sender_config));
 
     /* Setup frame format that we want to write to sender. */
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Setup network packets format that sender should generate. */
diff --git a/src/public_api/examples/basic_sender_sine_wave.c b/src/public_api/examples/basic_sender_sine_wave.c
index fb9235dac..484732b82 100644
--- a/src/public_api/examples/basic_sender_sine_wave.c
+++ b/src/public_api/examples/basic_sender_sine_wave.c
@@ -81,8 +81,9 @@ int main() {
     memset(&sender_config, 0, sizeof(sender_config));
 
     /* Setup frame format that we want to write to sender. */
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Setup network packets format that sender should generate. */
diff --git a/src/public_api/examples/plugin_plc.c b/src/public_api/examples/plugin_plc.c
index 5c65fc731..6fa2bb135 100644
--- a/src/public_api/examples/plugin_plc.c
+++ b/src/public_api/examples/plugin_plc.c
@@ -202,8 +202,9 @@ int main() {
 
     /* Setup frame encoding that we read from receiver.
      * Note that this encoding is different from the encoding used by PLC plugin. */
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Enable PLC plugin. */
diff --git a/src/public_api/examples/send_recv_1_sender_2_receivers.c b/src/public_api/examples/send_recv_1_sender_2_receivers.c
index 30255655c..c1a19eeb0 100644
--- a/src/public_api/examples/send_recv_1_sender_2_receivers.c
+++ b/src/public_api/examples/send_recv_1_sender_2_receivers.c
@@ -85,8 +85,9 @@ static void* receiver_loop(void* arg) {
     roc_receiver_config receiver_config;
     memset(&receiver_config, 0, sizeof(receiver_config));
 
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Make read operation blocking as we don't have our own clock. */
@@ -176,8 +177,9 @@ static void sender_loop() {
     roc_sender_config sender_config;
     memset(&sender_config, 0, sizeof(sender_config));
 
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     sender_config.fec_encoding = ROC_FEC_ENCODING_RS8M;
diff --git a/src/public_api/examples/send_recv_2_senders_1_receiver.c b/src/public_api/examples/send_recv_2_senders_1_receiver.c
index 530e3c690..e69db3f7b 100644
--- a/src/public_api/examples/send_recv_2_senders_1_receiver.c
+++ b/src/public_api/examples/send_recv_2_senders_1_receiver.c
@@ -68,8 +68,9 @@ static void receiver_loop() {
     roc_receiver_config receiver_config;
     memset(&receiver_config, 0, sizeof(receiver_config));
 
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Make read operation blocking as we don't have our own clock. */
@@ -179,8 +180,9 @@ static void* sender_loop(void* arg) {
     roc_sender_config sender_config;
     memset(&sender_config, 0, sizeof(sender_config));
 
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     sender_config.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
diff --git a/src/public_api/examples/send_recv_multicast.c b/src/public_api/examples/send_recv_multicast.c
index 5fafc44e1..990474c7e 100644
--- a/src/public_api/examples/send_recv_multicast.c
+++ b/src/public_api/examples/send_recv_multicast.c
@@ -68,8 +68,9 @@ static void* receiver_loop(void* arg) {
     roc_receiver_config receiver_config;
     memset(&receiver_config, 0, sizeof(receiver_config));
 
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Make read operation blocking as we don't have our own clock. */
@@ -193,8 +194,9 @@ static void sender_loop() {
     roc_sender_config sender_config;
     memset(&sender_config, 0, sizeof(sender_config));
 
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     sender_config.fec_encoding = ROC_FEC_ENCODING_RS8M;
diff --git a/src/public_api/examples/send_recv_rtp.c b/src/public_api/examples/send_recv_rtp.c
index 9e861aebe..fcac001f6 100644
--- a/src/public_api/examples/send_recv_rtp.c
+++ b/src/public_api/examples/send_recv_rtp.c
@@ -47,8 +47,9 @@ static void* receiver_loop(void* arg) {
     roc_receiver_config receiver_config;
     memset(&receiver_config, 0, sizeof(receiver_config));
 
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Make read operation blocking as we don't have our own clock. */
@@ -102,8 +103,9 @@ static void sender_loop(roc_context* context) {
     roc_sender_config sender_config;
     memset(&sender_config, 0, sizeof(sender_config));
 
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Disable FEC as we want to use bare RTP.  */
diff --git a/src/public_api/examples/send_recv_rtp_rtcp_fec.c b/src/public_api/examples/send_recv_rtp_rtcp_fec.c
index 981ccb2fb..514200194 100644
--- a/src/public_api/examples/send_recv_rtp_rtcp_fec.c
+++ b/src/public_api/examples/send_recv_rtp_rtcp_fec.c
@@ -48,8 +48,9 @@ static void* receiver_loop(void* arg) {
     roc_receiver_config receiver_config;
     memset(&receiver_config, 0, sizeof(receiver_config));
 
+    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    receiver_config.frame_encoding.bits = 32;
     receiver_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Make read operation blocking as we don't have our own clock. */
@@ -130,8 +131,9 @@ static void sender_loop(roc_context* context) {
     roc_sender_config sender_config;
     memset(&sender_config, 0, sizeof(sender_config));
 
+    sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+    sender_config.frame_encoding.bits = 32;
     sender_config.frame_encoding.rate = MY_SAMPLE_RATE;
-    sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
     sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
     /* Enable Reed-Solomon FEC scheme because we use ROC_PROTO_RTP_RS8M_SOURCE
diff --git a/src/public_api/include/roc/config.h b/src/public_api/include/roc/config.h
index d06a1a385..333d4689e 100644
--- a/src/public_api/include/roc/config.h
+++ b/src/public_api/include/roc/config.h
@@ -315,21 +315,65 @@ typedef enum roc_fec_encoding {
 } roc_fec_encoding;
 
 /** Sample format.
- * Defines how each sample is represented.
- * Does not define channels layout and sample rate.
+ * Defines how samples are encoded into binary form.
+ * Doesn't define sample width, sample rate, and channels layout - these parameters
+ * are configured separately via \ref roc_media_encoding.
  */
 typedef enum roc_format {
-    /** PCM floats.
-     * Uncompressed samples coded as 32-bit native-endian floats in range [-1; 1].
-     * Channels are interleaved, e.g. two channels are encoded as "L R L R ...".
+    /** Uncompressed interleaved PCM IEEE-754 floats.
+     *
+     * Multiple channels are interleaved, e.g. two channels are encoded as "L R L R ...".
+     *
+     * Endianess depends on the context when format is applied: roc_frame uses
+     * native-endian, network packets uses endian defined by the protocol.
+     *
+     * Supported bit widths: 32, 64.
+     * Supported rates: any.
+     * Supported channels: any.
+     */
+    ROC_FORMAT_PCM_IEEE_FLOAT = 10,
+
+    /** Uncompressed interleaved PCM signed integers in 2's complement notation.
+     *
+     * Multiple channels are interleaved, e.g. two channels are encoded as "L R L R ...".
+     *
+     * Endianess depends on the context when format is applied: roc_frame uses
+     * native-endian, network packets uses endian defined by the protocol.
+     *
+     * Supported bit widths: 8, 16, 24, 32, 64.
+     * Supported rates: any.
+     * Supported channels: any.
+     */
+    ROC_FORMAT_PCM_SIGNED_INT = 11,
+
+    /** Uncompressed interleaved PCM unsigned integers.
+     *
+     * Multiple channels are interleaved, e.g. two channels are encoded as "L R L R ...".
+     *
+     * Endianess depends on the context when format is applied: roc_frame uses
+     * native-endian, network packets uses endian defined by the protocol.
+     *
+     * Supported bit widths: 8, 16, 24, 32, 64.
+     * Supported rates: any.
+     * Supported channels: any.
      */
-    ROC_FORMAT_PCM_FLOAT32 = 1
+    ROC_FORMAT_PCM_UNSIGNED_INT = 12,
 } roc_format;
 
 /** Channel layout.
  * Defines number of channels and meaning of each channel.
  */
 typedef enum roc_channel_layout {
+    /** Mono.
+     * One channel with monophonic sound.
+     */
+    ROC_CHANNEL_LAYOUT_MONO = 1,
+
+    /** Stereo.
+     * Two channels: left, right.
+     */
+    ROC_CHANNEL_LAYOUT_STEREO = 2,
+
     /** Multi-track audio.
      *
      * In multitrack layout, stream contains multiple channels which represent
@@ -339,35 +383,34 @@ typedef enum roc_channel_layout {
      * The number of channels is arbitrary and is defined by \c tracks field of
      * \ref roc_media_encoding struct.
      */
-    ROC_CHANNEL_LAYOUT_MULTITRACK = 1,
-
-    /** Mono.
-     * One channel with monophonic sound.
-     */
-    ROC_CHANNEL_LAYOUT_MONO = 2,
-
-    /** Stereo.
-     * Two channels: left, right.
-     */
-    ROC_CHANNEL_LAYOUT_STEREO = 3,
+    ROC_CHANNEL_LAYOUT_MULTITRACK = 4
 } roc_channel_layout;
 
 /** Media encoding.
  * Defines format and parameters of samples encoded in frames or packets.
  */
 typedef struct roc_media_encoding {
+    /** Sample format.
+     * Defines sample binary representation.
+     * May place limitations to allowed rates, bits, and channels.
+     */
+    roc_format format;
+
+    /** Sample bit depth.
+     * Defines number of bits per sample (e.g. 16).
+     * Allowed values may be limited by \c format.
+     */
+    unsigned int bits;
+
     /** Sample frequency.
      * Defines number of samples per channel per second (e.g. 44100).
+     * Allowed values may be limited by \c format.
      */
     unsigned int rate;
 
-    /** Sample format.
-     * Defines sample precision and encoding.
-     */
-    roc_format format;
-
     /** Channel layout.
      * Defines number of channels and meaning of each channel.
+     * Allowed values may be limited by \c format.
      */
     roc_channel_layout channels;
 
diff --git a/src/public_api/include/roc/plugin.h b/src/public_api/include/roc/plugin.h
index b65d27507..d86c7f06f 100644
--- a/src/public_api/include/roc/plugin.h
+++ b/src/public_api/include/roc/plugin.h
@@ -104,8 +104,8 @@ enum {
  *    arbitrary values, unless it's known that only certain packet encoding
  *    may be used by sender
  *
- *  - \c format is always \ref ROC_FORMAT_PCM_FLOAT32, PLC plugin doesn't
- *    need to support other formats
+ *  - \c format is always \ref ROC_FORMAT_PCM_IEEE_FLOAT and \c bits is 32.
+ *    PLC plugin doesn't need to support other formats.
  *
  * **Registration**
  *
diff --git a/src/public_api/src/adapters.cpp b/src/public_api/src/adapters.cpp
index 7bccf5ad8..1291835d6 100644
--- a/src/public_api/src/adapters.cpp
+++ b/src/public_api/src/adapters.cpp
@@ -13,7 +13,7 @@
 #include "roc_address/interface.h"
 #include "roc_audio/channel_defs.h"
 #include "roc_audio/freq_estimator.h"
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_audio/resampler_config.h"
 #include "roc_core/attributes.h"
 #include "roc_core/log.h"
@@ -311,6 +311,10 @@ ROC_ATTR_NO_SANITIZE_UB
 bool sample_spec_from_user(audio::SampleSpec& out,
                            const roc_media_encoding& in,
                            bool is_network) {
+    if (!sample_format_from_user(out, in, is_network)) {
+        return false;
+    }
+
     if (in.rate != 0) {
         out.set_sample_rate(in.rate);
     } else {
@@ -320,13 +324,6 @@ bool sample_spec_from_user(audio::SampleSpec& out,
         return false;
     }
 
-    if (!sample_format_from_user(out, in.format, is_network)) {
-        roc_log(LogError,
-                "bad configuration: invalid roc_media_encoding.format:"
-                " should be valid enum value");
-        return false;
-    }
-
     if (in.channels != 0) {
         if (in.channels == ROC_CHANNEL_LAYOUT_MULTITRACK) {
             if (in.tracks == 0) {
@@ -371,17 +368,19 @@ bool sample_spec_from_user(audio::SampleSpec& out,
 bool sample_spec_to_user(roc_media_encoding& out, const audio::SampleSpec& in) {
     memset(&out, 0, sizeof(out));
 
-    if (!in.is_valid()) {
+    if (!in.is_complete()) {
+        roc_log(LogError, "bad configuration: invalid sample spec");
         return false;
     }
 
-    out.rate = (unsigned int)in.sample_rate();
-
-    if (!sample_format_to_user(out.format, in)) {
+    if (!sample_format_to_user(out, in)) {
         return false;
     }
 
+    out.rate = (unsigned int)in.sample_rate();
+
     if (!channel_set_to_user(out.channels, out.tracks, in.channel_set())) {
+        roc_log(LogError, "bad configuration: unsupported channel set");
         return false;
     }
 
@@ -389,38 +388,187 @@ bool sample_spec_to_user(roc_media_encoding& out, const audio::SampleSpec& in) {
 }
 
 ROC_ATTR_NO_SANITIZE_UB
-bool sample_format_from_user(audio::SampleSpec& out, roc_format in, bool is_network) {
-    switch (enum_from_user(in)) {
-    case ROC_FORMAT_PCM_FLOAT32:
-        out.set_sample_format(audio::SampleFormat_Pcm);
-        // TODO(gh-608): use PcmFormat_Float32_Be instead of PcmFormat_SInt16_Be
-        out.set_pcm_format(is_network ? audio::PcmFormat_SInt16_Be
-                                      : audio::PcmFormat_Float32);
-        return true;
-    }
+bool sample_format_from_user(audio::SampleSpec& out,
+                             const roc_media_encoding& in,
+                             bool is_network) {
+    out.set_format(audio::Format_Invalid);
+    out.set_pcm_subformat(audio::PcmSubformat_Invalid);
+
+    switch (enum_from_user(in.format)) {
+    case ROC_FORMAT_PCM_IEEE_FLOAT: {
+        out.set_format(audio::Format_Pcm);
+
+        switch (in.bits) {
+        case 32:
+            out.set_pcm_subformat(audio::PcmSubformat_Float32);
+            break;
+        case 64:
+            out.set_pcm_subformat(audio::PcmSubformat_Float64);
+            break;
+        default:
+            roc_log(LogError,
+                    "bad configuration: invalid roc_media_encoding.bits:"
+                    " ROC_FORMAT_PCM_IEEE_FLOAT doesn't support specified bits count: %u",
+                    (unsigned)in.bits);
+            return false;
+        }
 
-    return false;
-}
+        break;
+    } break; // ROC_FORMAT_PCM_IEEE_FLOAT
+
+    case ROC_FORMAT_PCM_SIGNED_INT: {
+        out.set_format(audio::Format_Pcm);
+
+        switch (in.bits) {
+        case 8:
+            out.set_pcm_subformat(audio::PcmSubformat_SInt8);
+            break;
+        case 16:
+            out.set_pcm_subformat(audio::PcmSubformat_SInt16);
+            break;
+        case 24:
+            out.set_pcm_subformat(audio::PcmSubformat_SInt24);
+            break;
+        case 32:
+            out.set_pcm_subformat(audio::PcmSubformat_SInt32);
+            break;
+        case 64:
+            out.set_pcm_subformat(audio::PcmSubformat_SInt64);
+            break;
+        default:
+            roc_log(LogError,
+                    "bad configuration: invalid roc_media_encoding.bits:"
+                    " ROC_FORMAT_PCM_SIGNED_INT doesn't support specified bits count: %u",
+                    (unsigned)in.bits);
+            return false;
+        }
 
-bool sample_format_to_user(roc_format& out, const audio::SampleSpec& in) {
-    if (in.sample_format() != audio::SampleFormat_Pcm) {
-        return false;
+        break;
+    } break; // ROC_FORMAT_PCM_SIGNED_INT
+
+    case ROC_FORMAT_PCM_UNSIGNED_INT: {
+        out.set_format(audio::Format_Pcm);
+
+        switch (in.bits) {
+        case 8:
+            out.set_pcm_subformat(audio::PcmSubformat_UInt8);
+            break;
+        case 16:
+            out.set_pcm_subformat(audio::PcmSubformat_UInt16);
+            break;
+        case 24:
+            out.set_pcm_subformat(audio::PcmSubformat_UInt24);
+            break;
+        case 32:
+            out.set_pcm_subformat(audio::PcmSubformat_UInt32);
+            break;
+        case 64:
+            out.set_pcm_subformat(audio::PcmSubformat_UInt64);
+            break;
+        default:
+            roc_log(
+                LogError,
+                "bad configuration: invalid roc_media_encoding.bits:"
+                " ROC_FORMAT_PCM_UNSIGNED_INT doesn't support specified bits count: %u",
+                (unsigned)in.bits);
+            return false;
+        }
+
+        break;
+    } break; // ROC_FORMAT_PCM_SIGNED_INT
     }
 
-    const audio::PcmTraits traits = audio::pcm_format_traits(in.pcm_format());
-    if (!traits.has_flags(audio::Pcm_IsNative)) {
+    if (out.format() == audio::Format_Invalid) {
+        roc_log(LogError,
+                "bad configuration: invalid roc_media_encoding.format:"
+                " should be enum value");
         return false;
     }
 
-    switch (traits.default_variant) {
-    case audio::PcmFormat_Float32:
-        out = ROC_FORMAT_PCM_FLOAT32;
-        return true;
+    if (out.is_pcm() && is_network) {
+        // Switch to big endian.
+        const audio::PcmTraits traits = audio::pcm_subformat_traits(out.pcm_subformat());
+        out.set_pcm_subformat(traits.be_variant);
+    }
+
+    return true;
+}
+
+bool sample_format_to_user(roc_media_encoding& out, const audio::SampleSpec& in) {
+    switch (in.format()) {
+    case audio::Format_Pcm: {
+        const audio::PcmTraits traits = audio::pcm_subformat_traits(in.pcm_subformat());
+        if (!traits.has_flags(audio::Pcm_IsNative)) {
+            roc_log(LogError, "bad configuration: unsupported pcm endian");
+            return false;
+        }
+
+        switch (traits.default_variant) {
+        case audio::PcmSubformat_SInt8:
+            out.format = ROC_FORMAT_PCM_SIGNED_INT;
+            out.bits = 8;
+            return true;
+        case audio::PcmSubformat_UInt8:
+            out.format = ROC_FORMAT_PCM_UNSIGNED_INT;
+            out.bits = 8;
+            return true;
+
+        case audio::PcmSubformat_SInt16:
+            out.format = ROC_FORMAT_PCM_SIGNED_INT;
+            out.bits = 16;
+            return true;
+        case audio::PcmSubformat_UInt16:
+            out.format = ROC_FORMAT_PCM_UNSIGNED_INT;
+            out.bits = 16;
+            return true;
+
+        case audio::PcmSubformat_SInt24:
+            out.format = ROC_FORMAT_PCM_SIGNED_INT;
+            out.bits = 24;
+            return true;
+        case audio::PcmSubformat_UInt24:
+            out.format = ROC_FORMAT_PCM_UNSIGNED_INT;
+            out.bits = 24;
+            return true;
+
+        case audio::PcmSubformat_SInt32:
+            out.format = ROC_FORMAT_PCM_SIGNED_INT;
+            out.bits = 32;
+            return true;
+        case audio::PcmSubformat_UInt32:
+            out.format = ROC_FORMAT_PCM_UNSIGNED_INT;
+            out.bits = 32;
+            return true;
+
+        case audio::PcmSubformat_SInt64:
+            out.format = ROC_FORMAT_PCM_SIGNED_INT;
+            out.bits = 64;
+            return true;
+        case audio::PcmSubformat_UInt64:
+            out.format = ROC_FORMAT_PCM_UNSIGNED_INT;
+            out.bits = 64;
+            return true;
+
+        case audio::PcmSubformat_Float32:
+            out.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+            out.bits = 32;
+            return true;
+        case audio::PcmSubformat_Float64:
+            out.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+            out.bits = 64;
+            return true;
+
+        default:
+            roc_log(LogError, "bad configuration: unsupported pcm format");
+            return false;
+        }
+    } break; // audio::SampleFormat_Pcm
 
     default:
         break;
     }
 
+    roc_log(LogError, "bad configuration: unsupported sample format");
     return false;
 }
 
diff --git a/src/public_api/src/adapters.h b/src/public_api/src/adapters.h
index ea52dc6ee..5e78ba509 100644
--- a/src/public_api/src/adapters.h
+++ b/src/public_api/src/adapters.h
@@ -38,8 +38,10 @@ bool sample_spec_from_user(audio::SampleSpec& out,
                            bool is_network);
 bool sample_spec_to_user(roc_media_encoding& out, const audio::SampleSpec& in);
 
-bool sample_format_from_user(audio::SampleSpec& out, roc_format in, bool is_network);
-bool sample_format_to_user(roc_format& out, const audio::SampleSpec& in);
+bool sample_format_from_user(audio::SampleSpec& out,
+                             const roc_media_encoding& in,
+                             bool is_network);
+bool sample_format_to_user(roc_media_encoding& out, const audio::SampleSpec& in);
 
 bool channel_set_from_user(audio::ChannelSet& out,
                            roc_channel_layout in_layout,
diff --git a/src/tests/public_api/test_context.cpp b/src/tests/public_api/test_context.cpp
index 284cd3955..43b9cb7a3 100644
--- a/src/tests/public_api/test_context.cpp
+++ b/src/tests/public_api/test_context.cpp
@@ -55,8 +55,9 @@ TEST(context, reference_counting) {
     {
         roc_sender_config sender_config;
         memset(&sender_config, 0, sizeof(sender_config));
+        sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_config.frame_encoding.bits = 32;
         sender_config.frame_encoding.rate = 44100;
-        sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
         sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
         sender_config.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
 
@@ -69,8 +70,9 @@ TEST(context, reference_counting) {
         {
             roc_receiver_config receiver_config;
             memset(&receiver_config, 0, sizeof(receiver_config));
+            receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+            receiver_config.frame_encoding.bits = 32;
             receiver_config.frame_encoding.rate = 44100;
-            receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
             receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
             roc_receiver* receiver = NULL;
             CHECK(roc_receiver_open(context, &receiver_config, &receiver) == 0);
diff --git a/src/tests/public_api/test_helpers/context.h b/src/tests/public_api/test_helpers/context.h
index 1edf1417e..c3e211d7c 100644
--- a/src/tests/public_api/test_helpers/context.h
+++ b/src/tests/public_api/test_helpers/context.h
@@ -44,11 +44,28 @@ class Context : public core::NonCopyable<> {
         return ctx_;
     }
 
-    void register_multitrack_encoding(int encoding_id, unsigned num_tracks) {
+    void register_custom_encoding(int encoding_id,
+                                  roc_format format,
+                                  unsigned bits,
+                                  unsigned rate,
+                                  roc_channel_layout channels) {
         roc_media_encoding encoding;
         memset(&encoding, 0, sizeof(encoding));
-        encoding.rate = SampleRate;
-        encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        encoding.format = format;
+        encoding.bits = bits;
+        encoding.rate = rate;
+        encoding.channels = channels;
+
+        CHECK(roc_context_register_encoding(ctx_, encoding_id, &encoding) == 0);
+    }
+
+    void
+    register_multitrack_encoding(int encoding_id, unsigned rate, unsigned num_tracks) {
+        roc_media_encoding encoding;
+        memset(&encoding, 0, sizeof(encoding));
+        encoding.format = ROC_FORMAT_PCM_SIGNED_INT;
+        encoding.bits = 16;
+        encoding.rate = rate;
         encoding.channels = ROC_CHANNEL_LAYOUT_MULTITRACK;
         encoding.tracks = num_tracks;
 
diff --git a/src/tests/public_api/test_helpers/utils.h b/src/tests/public_api/test_helpers/utils.h
index f571e72ba..bf5edc08d 100644
--- a/src/tests/public_api/test_helpers/utils.h
+++ b/src/tests/public_api/test_helpers/utils.h
@@ -18,8 +18,6 @@ namespace {
 enum {
     MaxBufSize = 5120,
 
-    SampleRate = 44100,
-
     SourcePackets = 10,
     RepairPackets = 7,
 
diff --git a/src/tests/public_api/test_loopback_encoder_2_decoder.cpp b/src/tests/public_api/test_loopback_encoder_2_decoder.cpp
index b527a9287..206f48558 100644
--- a/src/tests/public_api/test_loopback_encoder_2_decoder.cpp
+++ b/src/tests/public_api/test_loopback_encoder_2_decoder.cpp
@@ -26,6 +26,7 @@ namespace api {
 namespace {
 
 enum {
+    SampleRate = 44100,
     NoFlags = 0,
     FlagLosses = (1 << 0),
 };
@@ -46,23 +47,23 @@ TEST_GROUP(loopback_encoder_2_decoder) {
         CHECK(context);
 
         memset(&sender_conf, 0, sizeof(sender_conf));
-        sender_conf.frame_encoding.rate = test::SampleRate;
-        sender_conf.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        sender_conf.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_conf.frame_encoding.bits = 32;
+        sender_conf.frame_encoding.rate = SampleRate;
         sender_conf.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
         sender_conf.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
-        sender_conf.packet_length =
-            test::PacketSamples * 1000000000ull / test::SampleRate;
+        sender_conf.packet_length = test::PacketSamples * 1000000000ull / SampleRate;
         sender_conf.clock_source = ROC_CLOCK_SOURCE_INTERNAL;
 
         memset(&receiver_conf, 0, sizeof(receiver_conf));
-        receiver_conf.frame_encoding.rate = test::SampleRate;
-        receiver_conf.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        receiver_conf.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        receiver_conf.frame_encoding.bits = 32;
+        receiver_conf.frame_encoding.rate = SampleRate;
         receiver_conf.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
         receiver_conf.clock_source = ROC_CLOCK_SOURCE_INTERNAL;
         receiver_conf.latency_tuner_profile = ROC_LATENCY_TUNER_PROFILE_INTACT;
-        receiver_conf.target_latency = test::Latency * 1000000000ull / test::SampleRate;
-        receiver_conf.no_playback_timeout =
-            test::Timeout * 1000000000ull / test::SampleRate;
+        receiver_conf.target_latency = test::Latency * 1000000000ull / SampleRate;
+        receiver_conf.no_playback_timeout = test::Timeout * 1000000000ull / SampleRate;
     }
 
     void teardown() {
diff --git a/src/tests/public_api/test_loopback_sender_2_receiver.cpp b/src/tests/public_api/test_loopback_sender_2_receiver.cpp
index bac067904..939b722be 100644
--- a/src/tests/public_api/test_loopback_sender_2_receiver.cpp
+++ b/src/tests/public_api/test_loopback_sender_2_receiver.cpp
@@ -31,11 +31,12 @@ TEST_GROUP(loopback_sender_2_receiver) {
         sample_step = 1. / 32768.;
     }
 
-    void init_config(unsigned flags, unsigned frame_chans, unsigned packet_chans,
-                     int encoding_id = 0) {
+    void init_config(unsigned flags, unsigned sample_rate, unsigned frame_chans,
+                     unsigned packet_chans, int encoding_id = 0) {
         memset(&sender_conf, 0, sizeof(sender_conf));
-        sender_conf.frame_encoding.rate = test::SampleRate;
-        sender_conf.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        sender_conf.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_conf.frame_encoding.bits = 32;
+        sender_conf.frame_encoding.rate = sample_rate;
 
         if (flags & test::FlagMultitrack) {
             sender_conf.frame_encoding.channels = ROC_CHANNEL_LAYOUT_MULTITRACK;
@@ -67,8 +68,7 @@ TEST_GROUP(loopback_sender_2_receiver) {
             sender_conf.packet_encoding = (roc_packet_encoding)encoding_id;
         }
 
-        sender_conf.packet_length =
-            test::PacketSamples * 1000000000ull / test::SampleRate;
+        sender_conf.packet_length = test::PacketSamples * 1000000000ull / sample_rate;
         sender_conf.clock_source = ROC_CLOCK_SOURCE_INTERNAL;
 
         if (flags & test::FlagRS8M) {
@@ -84,8 +84,9 @@ TEST_GROUP(loopback_sender_2_receiver) {
         }
 
         memset(&receiver_conf, 0, sizeof(receiver_conf));
-        receiver_conf.frame_encoding.rate = test::SampleRate;
-        receiver_conf.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        receiver_conf.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        receiver_conf.frame_encoding.bits = 32;
+        receiver_conf.frame_encoding.rate = sample_rate;
 
         if (flags & test::FlagMultitrack) {
             receiver_conf.frame_encoding.channels = ROC_CHANNEL_LAYOUT_MULTITRACK;
@@ -105,9 +106,8 @@ TEST_GROUP(loopback_sender_2_receiver) {
 
         receiver_conf.clock_source = ROC_CLOCK_SOURCE_INTERNAL;
         receiver_conf.latency_tuner_profile = ROC_LATENCY_TUNER_PROFILE_INTACT;
-        receiver_conf.target_latency = test::Latency * 1000000000ull / test::SampleRate;
-        receiver_conf.no_playback_timeout =
-            test::Timeout * 1000000000ull / test::SampleRate;
+        receiver_conf.target_latency = test::Latency * 1000000000ull / sample_rate;
+        receiver_conf.no_playback_timeout = test::Timeout * 1000000000ull / sample_rate;
     }
 
     bool is_rs8m_supported() {
@@ -120,9 +120,9 @@ TEST_GROUP(loopback_sender_2_receiver) {
 };
 
 TEST(loopback_sender_2_receiver, bare_rtp) {
-    enum { Flags = 0, FrameChans = 2, PacketChans = 2 };
+    enum { Flags = 0, SampleRate = 44100, FrameChans = 2, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -143,9 +143,9 @@ TEST(loopback_sender_2_receiver, bare_rtp) {
 }
 
 TEST(loopback_sender_2_receiver, rtp_rtcp) {
-    enum { Flags = test::FlagRTCP, FrameChans = 2, PacketChans = 2 };
+    enum { Flags = test::FlagRTCP, SampleRate = 44100, FrameChans = 2, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -170,9 +170,9 @@ TEST(loopback_sender_2_receiver, rs8m_without_losses) {
         return;
     }
 
-    enum { Flags = test::FlagRS8M, FrameChans = 2, PacketChans = 2 };
+    enum { Flags = test::FlagRS8M, SampleRate = 44100, FrameChans = 2, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -199,11 +199,12 @@ TEST(loopback_sender_2_receiver, rs8m_with_losses) {
 
     enum {
         Flags = test::FlagRS8M | test::FlagLoseSomePkts,
+        SampleRate = 44100,
         FrameChans = 2,
         PacketChans = 2
     };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -233,9 +234,9 @@ TEST(loopback_sender_2_receiver, ldpc_without_losses) {
         return;
     }
 
-    enum { Flags = test::FlagLDPC, FrameChans = 2, PacketChans = 2 };
+    enum { Flags = test::FlagLDPC, SampleRate = 44100, FrameChans = 2, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -262,11 +263,12 @@ TEST(loopback_sender_2_receiver, ldpc_with_losses) {
 
     enum {
         Flags = test::FlagLDPC | test::FlagLoseSomePkts,
+        SampleRate = 44100,
         FrameChans = 2,
         PacketChans = 2
     };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -292,9 +294,9 @@ TEST(loopback_sender_2_receiver, ldpc_with_losses) {
 }
 
 TEST(loopback_sender_2_receiver, separate_context) {
-    enum { Flags = 0, FrameChans = 2, PacketChans = 2 };
+    enum { Flags = 0, SampleRate = 44100, FrameChans = 2, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context recv_context, send_context;
 
@@ -315,9 +317,9 @@ TEST(loopback_sender_2_receiver, separate_context) {
 }
 
 TEST(loopback_sender_2_receiver, multiple_senders_one_receiver_sequential) {
-    enum { Flags = 0, FrameChans = 2, PacketChans = 2 };
+    enum { Flags = 0, SampleRate = 44100, FrameChans = 2, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -350,9 +352,16 @@ TEST(loopback_sender_2_receiver, multiple_senders_one_receiver_sequential) {
 }
 
 TEST(loopback_sender_2_receiver, sender_slots) {
-    enum { Flags = 0, FrameChans = 2, PacketChans = 2, Slot1 = 1, Slot2 = 2 };
+    enum {
+        Flags = 0,
+        SampleRate = 44100,
+        FrameChans = 2,
+        PacketChans = 2,
+        Slot1 = 1,
+        Slot2 = 2
+    };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -384,9 +393,16 @@ TEST(loopback_sender_2_receiver, sender_slots) {
 }
 
 TEST(loopback_sender_2_receiver, receiver_slots_sequential) {
-    enum { Flags = 0, FrameChans = 2, PacketChans = 2, Slot1 = 1, Slot2 = 2 };
+    enum {
+        Flags = 0,
+        SampleRate = 44100,
+        FrameChans = 2,
+        PacketChans = 2,
+        Slot1 = 1,
+        Slot2 = 2
+    };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -420,9 +436,9 @@ TEST(loopback_sender_2_receiver, receiver_slots_sequential) {
 }
 
 TEST(loopback_sender_2_receiver, mono) {
-    enum { Flags = 0, FrameChans = 1, PacketChans = 1 };
+    enum { Flags = 0, SampleRate = 44100, FrameChans = 1, PacketChans = 1 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -443,9 +459,9 @@ TEST(loopback_sender_2_receiver, mono) {
 }
 
 TEST(loopback_sender_2_receiver, stereo_mono_stereo) {
-    enum { Flags = 0, FrameChans = 2, PacketChans = 1 };
+    enum { Flags = 0, SampleRate = 44100, FrameChans = 2, PacketChans = 1 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -466,9 +482,9 @@ TEST(loopback_sender_2_receiver, stereo_mono_stereo) {
 }
 
 TEST(loopback_sender_2_receiver, mono_stereo_mono) {
-    enum { Flags = 0, FrameChans = 1, PacketChans = 2 };
+    enum { Flags = 0, SampleRate = 44100, FrameChans = 1, PacketChans = 2 };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -488,19 +504,93 @@ TEST(loopback_sender_2_receiver, mono_stereo_mono) {
     sender.join();
 }
 
+TEST(loopback_sender_2_receiver, custom_encoding) {
+    enum {
+        Flags = 0,
+        SampleRate = 48000,
+        BitWidth = 24,
+        FrameChans = 1,
+        PacketChans = 2,
+        EncodingID = 100
+    };
+
+    init_config(Flags, SampleRate, FrameChans, PacketChans, EncodingID);
+
+    test::Context context;
+
+    context.register_custom_encoding(EncodingID, ROC_FORMAT_PCM_UNSIGNED_INT, BitWidth,
+                                     SampleRate, ROC_CHANNEL_LAYOUT_STEREO);
+
+    test::Receiver receiver(context, receiver_conf, sample_step, FrameChans,
+                            test::FrameSamples, Flags);
+
+    receiver.bind();
+
+    test::Sender sender(context, sender_conf, sample_step, FrameChans, test::FrameSamples,
+                        Flags);
+
+    sender.connect(receiver.source_endpoint(), NULL, NULL);
+
+    CHECK(sender.start());
+    receiver.receive();
+    sender.stop();
+    sender.join();
+}
+
+TEST(loopback_sender_2_receiver, custom_encoding_separate_contextx) {
+    enum {
+        Flags = 0,
+        SampleRate = 48000,
+        BitWidth = 24,
+        FrameChans = 1,
+        PacketChans = 2,
+        EncodingID = 100
+    };
+
+    init_config(Flags, SampleRate, FrameChans, PacketChans, EncodingID);
+
+    test::Context recv_context;
+
+    recv_context.register_custom_encoding(EncodingID, ROC_FORMAT_PCM_UNSIGNED_INT,
+                                          BitWidth, SampleRate,
+                                          ROC_CHANNEL_LAYOUT_STEREO);
+
+    test::Receiver receiver(recv_context, receiver_conf, sample_step, FrameChans,
+                            test::FrameSamples, Flags);
+
+    receiver.bind();
+
+    test::Context send_context;
+
+    send_context.register_custom_encoding(EncodingID, ROC_FORMAT_PCM_UNSIGNED_INT,
+                                          BitWidth, SampleRate,
+                                          ROC_CHANNEL_LAYOUT_STEREO);
+
+    test::Sender sender(send_context, sender_conf, sample_step, FrameChans,
+                        test::FrameSamples, Flags);
+
+    sender.connect(receiver.source_endpoint(), NULL, NULL);
+
+    CHECK(sender.start());
+    receiver.receive();
+    sender.stop();
+    sender.join();
+}
+
 TEST(loopback_sender_2_receiver, multitrack) {
     enum {
         Flags = test::FlagMultitrack,
+        SampleRate = 44100,
         FrameChans = 4,
         PacketChans = 4,
         EncodingID = 100
     };
 
-    init_config(Flags, FrameChans, PacketChans, EncodingID);
+    init_config(Flags, SampleRate, FrameChans, PacketChans, EncodingID);
 
     test::Context context;
 
-    context.register_multitrack_encoding(EncodingID, PacketChans);
+    context.register_multitrack_encoding(EncodingID, SampleRate, PacketChans);
 
     test::Receiver receiver(context, receiver_conf, sample_step, FrameChans,
                             test::FrameSamples, Flags);
@@ -521,17 +611,18 @@ TEST(loopback_sender_2_receiver, multitrack) {
 TEST(loopback_sender_2_receiver, multitrack_separate_contexts) {
     enum {
         Flags = test::FlagMultitrack,
+        SampleRate = 44100,
         FrameChans = 4,
         PacketChans = 4,
         EncodingID = 100
     };
 
-    init_config(Flags, FrameChans, PacketChans, EncodingID);
+    init_config(Flags, SampleRate, FrameChans, PacketChans, EncodingID);
 
     test::Context recv_context, send_context;
 
-    recv_context.register_multitrack_encoding(EncodingID, PacketChans);
-    send_context.register_multitrack_encoding(EncodingID, PacketChans);
+    recv_context.register_multitrack_encoding(EncodingID, SampleRate, PacketChans);
+    send_context.register_multitrack_encoding(EncodingID, SampleRate, PacketChans);
 
     test::Receiver receiver(recv_context, receiver_conf, sample_step, FrameChans,
                             test::FrameSamples, Flags);
@@ -553,12 +644,13 @@ TEST(loopback_sender_2_receiver, multitrack_separate_contexts) {
 TEST(loopback_sender_2_receiver, metrics_measurements) {
     enum {
         Flags = test::FlagNonStrict | test::FlagInfinite | test::FlagRTCP,
+        SampleRate = 44100,
         FrameChans = 2,
         PacketChans = 2,
         MaxSess = 10
     };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -651,12 +743,13 @@ TEST(loopback_sender_2_receiver, metrics_measurements) {
 TEST(loopback_sender_2_receiver, metrics_connections) {
     enum {
         Flags = test::FlagNonStrict | test::FlagInfinite | test::FlagRTCP,
+        SampleRate = 44100,
         FrameChans = 2,
         PacketChans = 2,
         MaxSess = 10
     };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
@@ -757,6 +850,7 @@ TEST(loopback_sender_2_receiver, metrics_connections) {
 TEST(loopback_sender_2_receiver, metrics_connections_slots) {
     enum {
         Flags = test::FlagNonStrict | test::FlagInfinite | test::FlagRTCP,
+        SampleRate = 44100,
         FrameChans = 2,
         PacketChans = 2,
         MaxSess = 10,
@@ -764,7 +858,7 @@ TEST(loopback_sender_2_receiver, metrics_connections_slots) {
         Slot2 = 2
     };
 
-    init_config(Flags, FrameChans, PacketChans);
+    init_config(Flags, SampleRate, FrameChans, PacketChans);
 
     test::Context context;
 
diff --git a/src/tests/public_api/test_plugin_plc.cpp b/src/tests/public_api/test_plugin_plc.cpp
index 3f4cf52e1..9c8e6142d 100644
--- a/src/tests/public_api/test_plugin_plc.cpp
+++ b/src/tests/public_api/test_plugin_plc.cpp
@@ -26,6 +26,7 @@ namespace api {
 namespace {
 
 enum {
+    SampleRate = 44100,
     Magic = 123456789,
     NumChans = 2,
     LookaheadSamples = 10,
@@ -75,8 +76,9 @@ void* test_plc_new(roc_plugin_plc* plugin, const roc_media_encoding* encoding) {
     roc_panic_if_not(plugin);
     roc_panic_if_not(encoding);
 
-    roc_panic_if_not(encoding->format == ROC_FORMAT_PCM_FLOAT32);
-    roc_panic_if_not(encoding->rate == test::SampleRate);
+    roc_panic_if_not(encoding->format == ROC_FORMAT_PCM_IEEE_FLOAT);
+    roc_panic_if_not(encoding->bits == 32);
+    roc_panic_if_not(encoding->rate == SampleRate);
     roc_panic_if_not(encoding->channels == ROC_CHANNEL_LAYOUT_STEREO);
 
     return new TestPlc((TestPlugin*)plugin);
@@ -182,13 +184,13 @@ TEST_GROUP(plugin_plc) {
 
     void setup() {
         memset(&sender_conf, 0, sizeof(sender_conf));
-        sender_conf.frame_encoding.rate = test::SampleRate;
-        sender_conf.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        sender_conf.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_conf.frame_encoding.bits = 32;
+        sender_conf.frame_encoding.rate = SampleRate;
         sender_conf.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
         sender_conf.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
-        sender_conf.packet_length =
-            test::PacketSamples * 1000000000ull / test::SampleRate;
+        sender_conf.packet_length = test::PacketSamples * 1000000000ull / SampleRate;
 
         sender_conf.fec_encoding = ROC_FEC_ENCODING_RS8M;
         sender_conf.fec_block_source_packets = test::SourcePackets;
@@ -197,8 +199,9 @@ TEST_GROUP(plugin_plc) {
         sender_conf.clock_source = ROC_CLOCK_SOURCE_INTERNAL;
 
         memset(&receiver_conf, 0, sizeof(receiver_conf));
-        receiver_conf.frame_encoding.rate = test::SampleRate;
-        receiver_conf.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
+        receiver_conf.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        receiver_conf.frame_encoding.bits = 32;
+        receiver_conf.frame_encoding.rate = SampleRate;
         receiver_conf.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
         receiver_conf.clock_source = ROC_CLOCK_SOURCE_INTERNAL;
@@ -207,11 +210,11 @@ TEST_GROUP(plugin_plc) {
         receiver_conf.plc_backend = (roc_plc_backend)PluginID;
 
         receiver_conf.latency_tuner_profile = ROC_LATENCY_TUNER_PROFILE_INTACT;
-        receiver_conf.target_latency = test::Latency * 1000000000ull / test::SampleRate;
+        receiver_conf.target_latency = test::Latency * 1000000000ull / SampleRate;
         receiver_conf.latency_tolerance =
-            test::Latency * 1000000000ull / test::SampleRate * 10000;
+            test::Latency * 1000000000ull / SampleRate * 10000;
         receiver_conf.no_playback_timeout =
-            test::Timeout * 1000000000ull / test::SampleRate * 10000;
+            test::Timeout * 1000000000ull / SampleRate * 10000;
     }
 
     bool is_rs8m_supported() {
diff --git a/src/tests/public_api/test_receiver.cpp b/src/tests/public_api/test_receiver.cpp
index a655f2a47..6e30de2f4 100644
--- a/src/tests/public_api/test_receiver.cpp
+++ b/src/tests/public_api/test_receiver.cpp
@@ -30,8 +30,9 @@ TEST_GROUP(receiver) {
         CHECK(context);
 
         memset(&receiver_config, 0, sizeof(receiver_config));
+        receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        receiver_config.frame_encoding.bits = 32;
         receiver_config.frame_encoding.rate = 44100;
-        receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
         receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
     }
 
diff --git a/src/tests/public_api/test_receiver_decoder.cpp b/src/tests/public_api/test_receiver_decoder.cpp
index 10718abf5..78714eff5 100644
--- a/src/tests/public_api/test_receiver_decoder.cpp
+++ b/src/tests/public_api/test_receiver_decoder.cpp
@@ -32,13 +32,15 @@ TEST_GROUP(receiver_decoder) {
         CHECK(context);
 
         memset(&receiver_config, 0, sizeof(receiver_config));
+        receiver_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        receiver_config.frame_encoding.bits = 32;
         receiver_config.frame_encoding.rate = 44100;
-        receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
         receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 
         memset(&sender_config, 0, sizeof(sender_config));
+        sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_config.frame_encoding.bits = 32;
         sender_config.frame_encoding.rate = 44100;
-        sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
         sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
         sender_config.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
         sender_config.fec_encoding = ROC_FEC_ENCODING_DISABLE;
diff --git a/src/tests/public_api/test_sender.cpp b/src/tests/public_api/test_sender.cpp
index bd811dc59..db5c426c8 100644
--- a/src/tests/public_api/test_sender.cpp
+++ b/src/tests/public_api/test_sender.cpp
@@ -30,8 +30,9 @@ TEST_GROUP(sender) {
         CHECK(context);
 
         memset(&sender_config, 0, sizeof(sender_config));
+        sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_config.frame_encoding.bits = 32;
         sender_config.frame_encoding.rate = 44100;
-        sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
         sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
         sender_config.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
         sender_config.fec_encoding = ROC_FEC_ENCODING_DISABLE;
diff --git a/src/tests/public_api/test_sender_encoder.cpp b/src/tests/public_api/test_sender_encoder.cpp
index 16c1e014f..d4932e911 100644
--- a/src/tests/public_api/test_sender_encoder.cpp
+++ b/src/tests/public_api/test_sender_encoder.cpp
@@ -30,8 +30,9 @@ TEST_GROUP(sender_encoder) {
         CHECK(context);
 
         memset(&sender_config, 0, sizeof(sender_config));
+        sender_config.frame_encoding.format = ROC_FORMAT_PCM_IEEE_FLOAT;
+        sender_config.frame_encoding.bits = 32;
         sender_config.frame_encoding.rate = 44100;
-        sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
         sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
         sender_config.packet_encoding = ROC_PACKET_ENCODING_AVP_L16_STEREO;
         sender_config.fec_encoding = ROC_FEC_ENCODING_DISABLE;
diff --git a/src/tests/roc_audio/test_channel_mapper_reader.cpp b/src/tests/roc_audio/test_channel_mapper_reader.cpp
index b16004449..e3272756b 100644
--- a/src/tests/roc_audio/test_channel_mapper_reader.cpp
+++ b/src/tests/roc_audio/test_channel_mapper_reader.cpp
@@ -115,9 +115,9 @@ TEST_GROUP(channel_mapper_reader) {};
 TEST(channel_mapper_reader, small_read_upmix) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     const core::nanoseconds_t start_ts = 1000000;
@@ -146,9 +146,9 @@ TEST(channel_mapper_reader, small_read_upmix) {
 TEST(channel_mapper_reader, small_read_downmix) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     const core::nanoseconds_t start_cts = 1000000;
@@ -177,9 +177,9 @@ TEST(channel_mapper_reader, small_read_downmix) {
 TEST(channel_mapper_reader, small_read_no_cts) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, in_spec);
@@ -205,9 +205,9 @@ TEST(channel_mapper_reader, small_read_no_cts) {
 // Request big frame when upmixing.
 // Duration is capped so that both input and output frames could fit max size.
 TEST(channel_mapper_reader, big_read_upmix) {
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     const core::nanoseconds_t start_cts = 1000000;
@@ -247,9 +247,9 @@ TEST(channel_mapper_reader, big_read_upmix) {
 // Request big frame when downmixing.
 // Duration is capped so that both input and output frames could fit max size.
 TEST(channel_mapper_reader, big_read_downmix) {
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     const core::nanoseconds_t start_cts = 1000000;
@@ -289,9 +289,9 @@ TEST(channel_mapper_reader, big_read_downmix) {
 // Same as above, but input frames don't have CTS
 // (because we don't call enable_timestamps).
 TEST(channel_mapper_reader, big_read_no_cts) {
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, in_spec);
@@ -324,9 +324,9 @@ TEST(channel_mapper_reader, big_read_no_cts) {
 TEST(channel_mapper_reader, forward_mode) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     test::MockReader mock_reader(frame_factory, in_spec);
@@ -352,9 +352,9 @@ TEST(channel_mapper_reader, forward_mode) {
 TEST(channel_mapper_reader, forward_error) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     test::MockReader mock_reader(frame_factory, in_spec);
@@ -378,9 +378,9 @@ TEST(channel_mapper_reader, forward_error) {
 TEST(channel_mapper_reader, forward_partial) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     test::MockReader mock_reader(frame_factory, in_spec);
@@ -402,9 +402,9 @@ TEST(channel_mapper_reader, forward_partial) {
 TEST(channel_mapper_reader, preallocated_buffer) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     const size_t buffer_list[] = {
diff --git a/src/tests/roc_audio/test_channel_mapper_writer.cpp b/src/tests/roc_audio/test_channel_mapper_writer.cpp
index 2408a5607..b440bb963 100644
--- a/src/tests/roc_audio/test_channel_mapper_writer.cpp
+++ b/src/tests/roc_audio/test_channel_mapper_writer.cpp
@@ -98,9 +98,9 @@ TEST_GROUP(channel_mapper_writer) {};
 TEST(channel_mapper_writer, small_write_upmix) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     test::MockWriter mock_writer;
@@ -128,9 +128,9 @@ TEST(channel_mapper_writer, small_write_upmix) {
 TEST(channel_mapper_writer, small_write_downmix) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockWriter mock_writer;
@@ -158,9 +158,9 @@ TEST(channel_mapper_writer, small_write_downmix) {
 TEST(channel_mapper_writer, small_write_no_cts) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockWriter mock_writer;
@@ -187,9 +187,9 @@ TEST(channel_mapper_writer, small_write_no_cts) {
 // Write big frame when upmixing.
 // It should be split into multiple writes to fit maximum size.
 TEST(channel_mapper_writer, big_write_upmix) {
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     test::MockWriter mock_writer;
@@ -222,9 +222,9 @@ TEST(channel_mapper_writer, big_write_upmix) {
 // Write big frame when downmixing.
 // It should be split into multiple writes to fit maximum size.
 TEST(channel_mapper_writer, big_write_downmix) {
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockWriter mock_writer;
@@ -256,9 +256,9 @@ TEST(channel_mapper_writer, big_write_downmix) {
 
 // Same as above, but input frames don't have CTS.
 TEST(channel_mapper_writer, big_write_no_cts) {
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockWriter mock_writer;
@@ -288,9 +288,9 @@ TEST(channel_mapper_writer, big_write_no_cts) {
 TEST(channel_mapper_writer, forward_error) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec in_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                              ChanOrder_Smpte, ChanMask_Surround_Mono);
-    const SampleSpec out_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     test::MockWriter mock_writer;
diff --git a/src/tests/roc_audio/test_channel_set.cpp b/src/tests/roc_audio/test_channel_set.cpp
index d1a3ec252..a345b745c 100644
--- a/src/tests/roc_audio/test_channel_set.cpp
+++ b/src/tests/roc_audio/test_channel_set.cpp
@@ -567,44 +567,42 @@ TEST(channel_set, to_string) {
     {
         ChannelSet ch_set;
 
-        STRCMP_EQUAL("<none n_ch=0>", channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<none 0 none>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set;
         ch_set.set_layout(ChanLayout_Surround);
         ch_set.set_order(ChanOrder_Smpte);
 
-        STRCMP_EQUAL("<surround smpte n_ch=0>", channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<surround smpte 0 none>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set(ChanLayout_Surround, ChanOrder_Smpte, ChanMask_Surround_Mono);
 
-        STRCMP_EQUAL("<surround smpte n_ch=1 ch=FC>", channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<surround smpte 1 FC>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set(ChanLayout_Surround, ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
-        STRCMP_EQUAL("<surround smpte n_ch=2 ch=FL,FR>",
-                     channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<surround smpte 2 FL,FR>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set(ChanLayout_Surround, ChanOrder_Alsa, ChanMask_Surround_Stereo);
 
-        STRCMP_EQUAL("<surround alsa n_ch=2 ch=FL,FR>",
-                     channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<surround alsa 2 FL,FR>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set;
         ch_set.set_layout(ChanLayout_Multitrack);
 
-        STRCMP_EQUAL("<multitrack n_ch=0>", channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<multitrack 0 none>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set;
         ch_set.set_layout(ChanLayout_Multitrack);
         ch_set.set_range(0, 7);
 
-        STRCMP_EQUAL("<multitrack n_ch=8 ch=0xFF>", channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<multitrack 8 0xFF>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set;
@@ -614,7 +612,7 @@ TEST(channel_set, to_string) {
         ch_set.toggle_channel(5, true);
         ch_set.toggle_channel(7, true);
 
-        STRCMP_EQUAL("<multitrack n_ch=4 ch=0xAC>", channel_set_to_str(ch_set).c_str());
+        STRCMP_EQUAL("<multitrack 4 0xAC>", channel_set_to_str(ch_set).c_str());
     }
     {
         ChannelSet ch_set;
@@ -624,7 +622,7 @@ TEST(channel_set, to_string) {
         ch_set.toggle_channel(85, true);
         ch_set.toggle_channel(87, true);
 
-        STRCMP_EQUAL("<multitrack n_ch=4 ch=0xA00000000000000000000C>",
+        STRCMP_EQUAL("<multitrack 4 0xA00000000000000000000C>",
                      channel_set_to_str(ch_set).c_str());
     }
 }
diff --git a/src/tests/roc_audio/test_depacketizer.cpp b/src/tests/roc_audio/test_depacketizer.cpp
index 14f0f8a22..d24edd48c 100644
--- a/src/tests/roc_audio/test_depacketizer.cpp
+++ b/src/tests/roc_audio/test_depacketizer.cpp
@@ -37,10 +37,10 @@ enum {
 };
 
 const SampleSpec frame_spec(
-    SampleRate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
+    SampleRate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
 const SampleSpec packet_spec(
-    SampleRate, PcmFormat_SInt16_Be, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
+    SampleRate, PcmSubformat_SInt16_Be, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
 const core::nanoseconds_t NsPerPacket =
     packet_spec.samples_per_chan_2_ns(SamplesPerPacket);
diff --git a/src/tests/roc_audio/test_fanout.cpp b/src/tests/roc_audio/test_fanout.cpp
index 6f494d5c3..86c396be5 100644
--- a/src/tests/roc_audio/test_fanout.cpp
+++ b/src/tests/roc_audio/test_fanout.cpp
@@ -23,7 +23,7 @@ namespace {
 enum { BufSz = 100, MaxSz = 500 };
 
 const SampleSpec sample_spec(44100,
-                             Sample_RawFormat,
+                             PcmSubformat_Raw,
                              ChanLayout_Surround,
                              ChanOrder_Smpte,
                              ChanMask_Surround_Stereo);
diff --git a/src/tests/roc_audio/test_frame_encoder_decoder.cpp b/src/tests/roc_audio/test_frame_encoder_decoder.cpp
index 8ceca9173..5b2d310db 100644
--- a/src/tests/roc_audio/test_frame_encoder_decoder.cpp
+++ b/src/tests/roc_audio/test_frame_encoder_decoder.cpp
@@ -11,7 +11,7 @@
 #include "roc_audio/frame_factory.h"
 #include "roc_audio/pcm_decoder.h"
 #include "roc_audio/pcm_encoder.h"
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/heap_arena.h"
 #include "roc_core/scoped_ptr.h"
 
@@ -51,25 +51,25 @@ IFrameEncoder* new_encoder(size_t id) {
     switch (id) {
     case Codec_PCM_SInt16_1ch:
         return new (arena)
-            PcmEncoder(SampleSpec(SampleRate, PcmFormat_SInt16_Be, ChanLayout_Surround,
+            PcmEncoder(SampleSpec(SampleRate, PcmSubformat_SInt16_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Mono),
                        arena);
 
     case Codec_PCM_SInt16_2ch:
         return new (arena)
-            PcmEncoder(SampleSpec(SampleRate, PcmFormat_SInt16_Be, ChanLayout_Surround,
+            PcmEncoder(SampleSpec(SampleRate, PcmSubformat_SInt16_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Stereo),
                        arena);
 
     case Codec_PCM_SInt24_1ch:
         return new (arena)
-            PcmEncoder(SampleSpec(SampleRate, PcmFormat_SInt24_Be, ChanLayout_Surround,
+            PcmEncoder(SampleSpec(SampleRate, PcmSubformat_SInt24_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Mono),
                        arena);
 
     case Codec_PCM_SInt24_2ch:
         return new (arena)
-            PcmEncoder(SampleSpec(SampleRate, PcmFormat_SInt24_Be, ChanLayout_Surround,
+            PcmEncoder(SampleSpec(SampleRate, PcmSubformat_SInt24_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Stereo),
                        arena);
 
@@ -84,25 +84,25 @@ IFrameDecoder* new_decoder(size_t id) {
     switch (id) {
     case Codec_PCM_SInt16_1ch:
         return new (arena)
-            PcmDecoder(SampleSpec(SampleRate, PcmFormat_SInt16_Be, ChanLayout_Surround,
+            PcmDecoder(SampleSpec(SampleRate, PcmSubformat_SInt16_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Mono),
                        arena);
 
     case Codec_PCM_SInt16_2ch:
         return new (arena)
-            PcmDecoder(SampleSpec(SampleRate, PcmFormat_SInt16_Be, ChanLayout_Surround,
+            PcmDecoder(SampleSpec(SampleRate, PcmSubformat_SInt16_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Stereo),
                        arena);
 
     case Codec_PCM_SInt24_1ch:
         return new (arena)
-            PcmDecoder(SampleSpec(SampleRate, PcmFormat_SInt24_Be, ChanLayout_Surround,
+            PcmDecoder(SampleSpec(SampleRate, PcmSubformat_SInt24_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Mono),
                        arena);
 
     case Codec_PCM_SInt24_2ch:
         return new (arena)
-            PcmDecoder(SampleSpec(SampleRate, PcmFormat_SInt24_Be, ChanLayout_Surround,
+            PcmDecoder(SampleSpec(SampleRate, PcmSubformat_SInt24_Be, ChanLayout_Surround,
                                   ChanOrder_Smpte, ChanMask_Surround_Stereo),
                        arena);
 
diff --git a/src/tests/roc_audio/test_freq_estimator.cpp b/src/tests/roc_audio/test_freq_estimator.cpp
index 4fe53e53d..12df71863 100644
--- a/src/tests/roc_audio/test_freq_estimator.cpp
+++ b/src/tests/roc_audio/test_freq_estimator.cpp
@@ -24,7 +24,7 @@ const LatencyTunerProfile profile_list[] = { LatencyTunerProfile_Responsive,
                                              LatencyTunerProfile_Gradual };
 
 const SampleSpec sample_spec(44100,
-                             Sample_RawFormat,
+                             PcmSubformat_Raw,
                              ChanLayout_Surround,
                              ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
diff --git a/src/tests/roc_audio/test_mixer.cpp b/src/tests/roc_audio/test_mixer.cpp
index 61b05a7df..64ba00d17 100644
--- a/src/tests/roc_audio/test_mixer.cpp
+++ b/src/tests/roc_audio/test_mixer.cpp
@@ -22,7 +22,7 @@ namespace {
 enum { BufSz = 100, MaxBufSz = 500, SampleRate = 44100 };
 
 const SampleSpec sample_spec(SampleRate,
-                             Sample_RawFormat,
+                             PcmSubformat_Raw,
                              ChanLayout_Surround,
                              ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
@@ -333,7 +333,7 @@ TEST(mixer, clamp) {
 
 TEST(mixer, cts_one_reader) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts = 1000000000000;
 
@@ -362,7 +362,7 @@ TEST(mixer, cts_one_reader) {
 
 TEST(mixer, cts_two_readers) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts1 = 2000000000000;
     const core::nanoseconds_t start_ts2 = 1000000000000;
@@ -400,7 +400,7 @@ TEST(mixer, cts_two_readers) {
 
 TEST(mixer, cts_partial) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts1 = 2000000000000;
     const core::nanoseconds_t start_ts2 = 1000000000000;
@@ -445,7 +445,7 @@ TEST(mixer, cts_partial) {
 
 TEST(mixer, cts_prevent_overflow) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts1 = 9000000000000000000ll;
     const core::nanoseconds_t start_ts2 = 9100000000000000000ll;
@@ -487,7 +487,7 @@ TEST(mixer, cts_prevent_overflow) {
 }
 
 TEST(mixer, cts_disabled) {
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts = 1000000000000;
 
@@ -735,7 +735,7 @@ TEST(mixer, soft_read_partial_end_two_readers) {
 // Soft reads and capture timestamps.
 TEST(mixer, soft_read_cts) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts = 1000000000000;
 
@@ -766,7 +766,7 @@ TEST(mixer, soft_read_cts) {
 // Soft reads and capture timestamps.
 TEST(mixer, soft_read_cts_partial) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts = 1000000000000;
 
@@ -798,7 +798,7 @@ TEST(mixer, soft_read_cts_partial) {
 // Same as above, but there are two readers, and one returns StatusDrain.
 TEST(mixer, soft_read_cts_two_readers) {
     // BufSz samples per second
-    const SampleSpec sample_spec(BufSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(BufSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
     const core::nanoseconds_t start_ts1 = 2000000000000;
     const core::nanoseconds_t start_ts2 = 1000000000000;
diff --git a/src/tests/roc_audio/test_packetizer.cpp b/src/tests/roc_audio/test_packetizer.cpp
index af4dd03d0..61e071525 100644
--- a/src/tests/roc_audio/test_packetizer.cpp
+++ b/src/tests/roc_audio/test_packetizer.cpp
@@ -44,10 +44,10 @@ const core::nanoseconds_t PacketDuration = SamplesPerPacket * core::Second / Sam
 const core::nanoseconds_t Now = 1691499037871419405;
 
 const SampleSpec frame_spec(
-    SampleRate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
+    SampleRate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
 const SampleSpec packet_spec(
-    SampleRate, PcmFormat_SInt16_Be, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
+    SampleRate, PcmSubformat_SInt16_Be, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
 core::HeapArena arena;
 packet::PacketFactory packet_factory(arena, MaxBufSize);
diff --git a/src/tests/roc_audio/test_pcm_mapper.cpp b/src/tests/roc_audio/test_pcm_mapper.cpp
index 073e78932..e14552225 100644
--- a/src/tests/roc_audio/test_pcm_mapper.cpp
+++ b/src/tests/roc_audio/test_pcm_mapper.cpp
@@ -29,8 +29,8 @@ void map(const void* input,
          size_t in_bytes,
          size_t out_bytes,
          size_t n_samples,
-         PcmFormat in_fmt,
-         PcmFormat out_fmt) {
+         PcmSubformat in_fmt,
+         PcmSubformat out_fmt) {
     PcmMapper mapper(in_fmt, out_fmt);
 
     UNSIGNED_LONGS_EQUAL(n_samples, mapper.input_sample_count(in_bytes));
@@ -108,7 +108,7 @@ TEST(pcm_mapper, raw_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, Sample_RawFormat);
+        PcmSubformat_Raw, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -126,7 +126,7 @@ TEST(pcm_mapper, raw_to_int8) {
     int8_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt8);
+        PcmSubformat_Raw, PcmSubformat_SInt8);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -144,7 +144,7 @@ TEST(pcm_mapper, int8_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt8, Sample_RawFormat);
+        PcmSubformat_SInt8, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -162,7 +162,7 @@ TEST(pcm_mapper, raw_to_int16) {
     int16_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt16);
+        PcmSubformat_Raw, PcmSubformat_SInt16);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -180,7 +180,7 @@ TEST(pcm_mapper, int16_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt16, Sample_RawFormat);
+        PcmSubformat_SInt16, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -208,7 +208,7 @@ TEST(pcm_mapper, raw_to_int32) {
     int32_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt32);
+        PcmSubformat_Raw, PcmSubformat_SInt32);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -238,7 +238,7 @@ TEST(pcm_mapper, int32_to_raw) {
     float actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt32, Sample_RawFormat);
+        PcmSubformat_SInt32, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -264,7 +264,7 @@ TEST(pcm_mapper, raw_to_int64) {
     int64_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt64);
+        PcmSubformat_Raw, PcmSubformat_SInt64);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -290,7 +290,7 @@ TEST(pcm_mapper, int64_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt64, Sample_RawFormat);
+        PcmSubformat_SInt64, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -308,7 +308,7 @@ TEST(pcm_mapper, raw_to_float32) {
     float actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_Float32);
+        PcmSubformat_Raw, PcmSubformat_Float32);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -326,7 +326,7 @@ TEST(pcm_mapper, float32_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_Float32, Sample_RawFormat);
+        PcmSubformat_Float32, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -344,7 +344,7 @@ TEST(pcm_mapper, raw_to_float64) {
     double actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_Float64);
+        PcmSubformat_Raw, PcmSubformat_Float64);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -362,7 +362,7 @@ TEST(pcm_mapper, float64_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_Float64, Sample_RawFormat);
+        PcmSubformat_Float64, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -392,7 +392,7 @@ TEST(pcm_mapper, raw_to_uint16) {
     uint16_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_UInt16);
+        PcmSubformat_Raw, PcmSubformat_UInt16);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -418,7 +418,7 @@ TEST(pcm_mapper, uint16_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_UInt16, Sample_RawFormat);
+        PcmSubformat_UInt16, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -446,7 +446,7 @@ TEST(pcm_mapper, raw_to_uint32) {
     uint32_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_UInt32);
+        PcmSubformat_Raw, PcmSubformat_UInt32);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -476,7 +476,7 @@ TEST(pcm_mapper, uint32_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_UInt32, Sample_RawFormat);
+        PcmSubformat_UInt32, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -501,7 +501,7 @@ TEST(pcm_mapper, raw_to_int16be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt16_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt16_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -526,7 +526,7 @@ TEST(pcm_mapper, raw_to_int16le) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt16_Le);
+        PcmSubformat_Raw, PcmSubformat_SInt16_Le);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -548,7 +548,7 @@ TEST(pcm_mapper, int16be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt16_Be, Sample_RawFormat);
+        PcmSubformat_SInt16_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -570,7 +570,7 @@ TEST(pcm_mapper, int16le_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt16_Le, Sample_RawFormat);
+        PcmSubformat_SInt16_Le, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -595,7 +595,7 @@ TEST(pcm_mapper, raw_to_int18b4be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt18_4_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt18_4_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -620,7 +620,7 @@ TEST(pcm_mapper, int18b4be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt18_4_Be, Sample_RawFormat);
+        PcmSubformat_SInt18_4_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -645,7 +645,7 @@ TEST(pcm_mapper, raw_to_int20b3be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt20_3_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt20_3_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -667,7 +667,7 @@ TEST(pcm_mapper, int20b3be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt20_3_Be, Sample_RawFormat);
+        PcmSubformat_SInt20_3_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -692,7 +692,7 @@ TEST(pcm_mapper, raw_to_int20b4be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt20_4_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt20_4_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -714,7 +714,7 @@ TEST(pcm_mapper, int20b4be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt20_4_Be, Sample_RawFormat);
+        PcmSubformat_SInt20_4_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -739,7 +739,7 @@ TEST(pcm_mapper, raw_to_int24be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt24_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt24_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -761,7 +761,7 @@ TEST(pcm_mapper, int24be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt24_Be, Sample_RawFormat);
+        PcmSubformat_SInt24_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -786,7 +786,7 @@ TEST(pcm_mapper, raw_to_int24b4be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt24_4_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt24_4_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -808,7 +808,7 @@ TEST(pcm_mapper, int24b4be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt24_4_Be, Sample_RawFormat);
+        PcmSubformat_SInt24_4_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
@@ -843,7 +843,7 @@ TEST(pcm_mapper, raw_to_int20be) {
     uint8_t actual_output[NumOutputBytes] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        Sample_RawFormat, PcmFormat_SInt20_Be);
+        PcmSubformat_Raw, PcmSubformat_SInt20_Be);
 
     compare(expected_output, actual_output, NumOutputBytes);
 }
@@ -875,7 +875,7 @@ TEST(pcm_mapper, int20be_to_raw) {
     sample_t actual_output[NumSamples] = {};
 
     map(input, actual_output, sizeof(input), sizeof(actual_output), NumSamples,
-        PcmFormat_SInt20_Be, Sample_RawFormat);
+        PcmSubformat_SInt20_Be, PcmSubformat_Raw);
 
     compare(expected_output, actual_output, NumSamples);
 }
diff --git a/src/tests/roc_audio/test_pcm_mapper_reader.cpp b/src/tests/roc_audio/test_pcm_mapper_reader.cpp
index 977da14fe..15947a285 100644
--- a/src/tests/roc_audio/test_pcm_mapper_reader.cpp
+++ b/src/tests/roc_audio/test_pcm_mapper_reader.cpp
@@ -8,8 +8,8 @@
 
 #include <CppUTest/TestHarness.h>
 
-#include "roc_audio/pcm_format.h"
 #include "roc_audio/pcm_mapper_reader.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/heap_arena.h"
 #include "roc_core/macro_helpers.h"
 #include "roc_core/time.h"
@@ -206,9 +206,9 @@ TEST_GROUP(pcm_mapper_reader) {};
 TEST(pcm_mapper_reader, mono_raw_to_raw) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     CountReader<sample_t> count_reader(in_spec, 0.001f);
@@ -231,9 +231,9 @@ TEST(pcm_mapper_reader, mono_raw_to_raw) {
 TEST(pcm_mapper_reader, mono_s16_to_raw) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     CountReader<int16_t> count_reader(in_spec, 100);
@@ -256,9 +256,9 @@ TEST(pcm_mapper_reader, mono_s16_to_raw) {
 TEST(pcm_mapper_reader, mono_raw_to_s16) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     CountReader<sample_t> count_reader(in_spec, 0.001f);
@@ -281,9 +281,9 @@ TEST(pcm_mapper_reader, mono_raw_to_s16) {
 TEST(pcm_mapper_reader, stereo_s16_to_raw) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Stereo);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     CountReader<int16_t> count_reader(in_spec, 100);
@@ -306,9 +306,9 @@ TEST(pcm_mapper_reader, stereo_s16_to_raw) {
 TEST(pcm_mapper_reader, stereo_raw_to_s16) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     CountReader<sample_t> count_reader(in_spec, 0.001f);
@@ -336,9 +336,9 @@ TEST(pcm_mapper_reader, big_read_s16_to_raw) {
         MaxFrameSz = MaxBytes / ROC_MAX(sizeof(int16_t), sizeof(sample_t))
     };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     CountReader<int16_t> count_reader(in_spec, 10);
@@ -370,9 +370,9 @@ TEST(pcm_mapper_reader, big_read_raw_to_s16) {
         MaxFrameSz = MaxBytes / ROC_MAX(sizeof(int16_t), sizeof(sample_t))
     };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     CountReader<sample_t> count_reader(in_spec, 0.001f);
@@ -401,9 +401,9 @@ TEST(pcm_mapper_reader, big_read_raw_to_s16) {
 TEST(pcm_mapper_reader, forward_flags) {
     enum { MaxFrameSz = MaxBytes / sizeof(sample_t) };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaReader meta_reader(in_spec);
@@ -428,9 +428,9 @@ TEST(pcm_mapper_reader, forward_flags) {
 TEST(pcm_mapper_reader, forward_capture_timestamp) {
     enum { MaxFrameSz = MaxBytes / sizeof(sample_t) };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaReader meta_reader(in_spec);
@@ -455,9 +455,9 @@ TEST(pcm_mapper_reader, forward_capture_timestamp) {
 TEST(pcm_mapper_reader, forward_mode) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaReader meta_reader(in_spec);
@@ -481,9 +481,9 @@ TEST(pcm_mapper_reader, forward_mode) {
 TEST(pcm_mapper_reader, forward_error) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaReader meta_reader(in_spec);
@@ -507,9 +507,9 @@ TEST(pcm_mapper_reader, forward_error) {
 TEST(pcm_mapper_reader, forward_partial) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     CountReader<int16_t> count_reader(in_spec, 100);
@@ -531,9 +531,9 @@ TEST(pcm_mapper_reader, forward_partial) {
 TEST(pcm_mapper_reader, preallocated_buffer) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     const size_t buffer_list[] = {
diff --git a/src/tests/roc_audio/test_pcm_mapper_writer.cpp b/src/tests/roc_audio/test_pcm_mapper_writer.cpp
index f0fce4849..e453f1478 100644
--- a/src/tests/roc_audio/test_pcm_mapper_writer.cpp
+++ b/src/tests/roc_audio/test_pcm_mapper_writer.cpp
@@ -8,8 +8,8 @@
 
 #include <CppUTest/TestHarness.h>
 
-#include "roc_audio/pcm_format.h"
 #include "roc_audio/pcm_mapper_writer.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/heap_arena.h"
 #include "roc_core/macro_helpers.h"
 #include "roc_core/time.h"
@@ -191,9 +191,9 @@ TEST_GROUP(pcm_mapper_writer) {};
 TEST(pcm_mapper_writer, mono_raw_to_raw) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     BufferWriter<sample_t> buf_writer(out_spec);
@@ -214,9 +214,9 @@ TEST(pcm_mapper_writer, mono_raw_to_raw) {
 TEST(pcm_mapper_writer, mono_s16_to_raw) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     BufferWriter<sample_t> buf_writer(out_spec);
@@ -237,9 +237,9 @@ TEST(pcm_mapper_writer, mono_s16_to_raw) {
 TEST(pcm_mapper_writer, mono_raw_to_s16) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     BufferWriter<int16_t> buf_writer(out_spec);
@@ -260,9 +260,9 @@ TEST(pcm_mapper_writer, mono_raw_to_s16) {
 TEST(pcm_mapper_writer, stereo_s16_to_raw) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Stereo);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     BufferWriter<sample_t> buf_writer(out_spec);
@@ -283,9 +283,9 @@ TEST(pcm_mapper_writer, stereo_s16_to_raw) {
 TEST(pcm_mapper_writer, stereo_raw_to_s16) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Stereo);
-    const SampleSpec out_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
     BufferWriter<int16_t> buf_writer(out_spec);
@@ -308,9 +308,9 @@ TEST(pcm_mapper_writer, stereo_raw_to_s16) {
 TEST(pcm_mapper_writer, big_write_s16_to_raw) {
     enum { IterCount = 20, SplitCount = 5, MaxFrameSz = MaxBytes / sizeof(sample_t) };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     BufferWriter<sample_t> buf_writer(out_spec);
@@ -335,9 +335,9 @@ TEST(pcm_mapper_writer, big_write_s16_to_raw) {
 TEST(pcm_mapper_writer, big_write_raw_to_s16) {
     enum { IterCount = 20, SplitCount = 5, MaxFrameSz = MaxBytes / sizeof(int16_t) };
 
-    const SampleSpec in_spec(Rate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+    const SampleSpec in_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                              ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec out_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     BufferWriter<int16_t> buf_writer(out_spec);
@@ -363,9 +363,9 @@ TEST(pcm_mapper_writer, big_write_raw_to_s16) {
 TEST(pcm_mapper_writer, forward_flags) {
     enum { MaxFrameSz = MaxBytes / sizeof(int16_t) };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaWriter meta_writer(out_spec);
@@ -391,9 +391,9 @@ TEST(pcm_mapper_writer, forward_flags) {
 TEST(pcm_mapper_writer, forward_capture_timestamp) {
     enum { MaxFrameSz = MaxBytes / sizeof(int16_t) };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaWriter meta_writer(out_spec);
@@ -419,9 +419,9 @@ TEST(pcm_mapper_writer, forward_capture_timestamp) {
 TEST(pcm_mapper_writer, forward_error) {
     enum { FrameSz = MaxBytes / 10 };
 
-    const SampleSpec in_spec(Rate, PcmFormat_SInt16, ChanLayout_Surround, ChanOrder_Smpte,
-                             ChanMask_Surround_Mono);
-    const SampleSpec out_spec(Rate, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec in_spec(Rate, PcmSubformat_SInt16, ChanLayout_Surround,
+                             ChanOrder_Smpte, ChanMask_Surround_Mono);
+    const SampleSpec out_spec(Rate, PcmSubformat_Raw, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     MetaWriter meta_writer(out_spec);
diff --git a/src/tests/roc_audio/test_pcm_samples.cpp b/src/tests/roc_audio/test_pcm_samples.cpp
index ef8a9d162..37c1ce902 100644
--- a/src/tests/roc_audio/test_pcm_samples.cpp
+++ b/src/tests/roc_audio/test_pcm_samples.cpp
@@ -59,8 +59,8 @@ TEST(pcm_samples, decode) {
     for (size_t idx = 0; idx < ROC_ARRAY_SIZE(test_samples); idx++) {
         roc_log(LogDebug, "mapping %s to raw samples", test_samples[idx]->name);
 
-        PcmFormat in_fmt = test_samples[idx]->format;
-        PcmFormat out_fmt = Sample_RawFormat;
+        PcmSubformat in_fmt = test_samples[idx]->format;
+        PcmSubformat out_fmt = PcmSubformat_Raw;
 
         PcmMapper mapper(in_fmt, out_fmt);
 
@@ -103,8 +103,8 @@ TEST(pcm_samples, encode_decode) {
         { // encode
             roc_log(LogDebug, "mapping raw samples to %s", test_samples[idx]->name);
 
-            PcmFormat in_fmt = Sample_RawFormat;
-            PcmFormat out_fmt = test_samples[idx]->format;
+            PcmSubformat in_fmt = PcmSubformat_Raw;
+            PcmSubformat out_fmt = test_samples[idx]->format;
 
             PcmMapper mapper(in_fmt, out_fmt);
 
@@ -135,8 +135,8 @@ TEST(pcm_samples, encode_decode) {
         { // decode
             roc_log(LogDebug, "mapping %s to raw samples", test_samples[idx]->name);
 
-            PcmFormat in_fmt = test_samples[idx]->format;
-            PcmFormat out_fmt = Sample_RawFormat;
+            PcmSubformat in_fmt = test_samples[idx]->format;
+            PcmSubformat out_fmt = PcmSubformat_Raw;
 
             PcmMapper mapper(in_fmt, out_fmt);
 
diff --git a/src/tests/roc_audio/test_plc_reader.cpp b/src/tests/roc_audio/test_plc_reader.cpp
index 42fef37a6..6c8cbdce2 100644
--- a/src/tests/roc_audio/test_plc_reader.cpp
+++ b/src/tests/roc_audio/test_plc_reader.cpp
@@ -385,7 +385,7 @@ TEST_GROUP(plc_reader) {
 TEST(plc_reader, small_read) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -414,7 +414,7 @@ TEST(plc_reader, small_read) {
 TEST(plc_reader, big_read) {
     enum { FrameSz = MaxSz * 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -443,7 +443,7 @@ TEST(plc_reader, big_read) {
 TEST(plc_reader, initial_gap) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -496,7 +496,7 @@ TEST(plc_reader, initial_gap) {
 TEST(plc_reader, readahead_disabled) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -568,7 +568,7 @@ TEST(plc_reader, readahead_disabled) {
 TEST(plc_reader, readahead_enabled) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -675,7 +675,7 @@ TEST(plc_reader, readahead_enabled) {
 TEST(plc_reader, readahead_drained) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -749,7 +749,7 @@ TEST(plc_reader, readahead_drained) {
 TEST(plc_reader, readahead_partial) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -826,7 +826,7 @@ TEST(plc_reader, readahead_partial) {
 TEST(plc_reader, soft_reads) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -967,7 +967,7 @@ TEST(plc_reader, variable_frame_sizes) {
         LookaheadSz = 13,
     };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -1130,7 +1130,7 @@ TEST(plc_reader, variable_frame_sizes) {
 TEST(plc_reader, without_cts) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -1198,7 +1198,7 @@ TEST(plc_reader, without_cts) {
 TEST(plc_reader, with_cts) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -1272,7 +1272,7 @@ TEST(plc_reader, with_cts) {
 TEST(plc_reader, non_raw_format) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec int_spec(MaxSz, PcmFormat_SInt16, ChanLayout_Surround,
+    const SampleSpec int_spec(MaxSz, PcmSubformat_SInt16, ChanLayout_Surround,
                               ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     IntReader<int16_t> int_reader(int_spec);
@@ -1354,7 +1354,7 @@ TEST(plc_reader, non_raw_format) {
 TEST(plc_reader, supported_backends) {
     enum { FrameSz = MaxSz / 2, NumFrames = 5, NumIters = 10 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     for (size_t n_back = 0; n_back < n_supported_backends; n_back++) {
@@ -1401,7 +1401,7 @@ TEST(plc_reader, supported_backends) {
 TEST(plc_reader, forward_mode) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -1429,7 +1429,7 @@ TEST(plc_reader, forward_mode) {
 TEST(plc_reader, forward_error) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -1457,7 +1457,7 @@ TEST(plc_reader, forward_error) {
 TEST(plc_reader, forward_partial) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     test::MockReader mock_reader(frame_factory, sample_spec);
@@ -1481,7 +1481,7 @@ TEST(plc_reader, forward_partial) {
 TEST(plc_reader, preallocated_buffer) {
     enum { FrameSz = MaxSz / 2 };
 
-    const SampleSpec sample_spec(MaxSz, Sample_RawFormat, ChanLayout_Surround,
+    const SampleSpec sample_spec(MaxSz, PcmSubformat_Raw, ChanLayout_Surround,
                                  ChanOrder_Smpte, ChanMask_Surround_Mono);
 
     const size_t buffer_list[] = {
diff --git a/src/tests/roc_audio/test_profiler.cpp b/src/tests/roc_audio/test_profiler.cpp
index bf3b0f03d..0402466a8 100644
--- a/src/tests/roc_audio/test_profiler.cpp
+++ b/src/tests/roc_audio/test_profiler.cpp
@@ -31,7 +31,7 @@ const int SampleRate = 5000; // 50 samples / chunk
 const int ChannelMask = 0x1;
 
 const SampleSpec sample_spec(
-    SampleRate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte, ChannelMask);
+    SampleRate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte, ChannelMask);
 
 const ProfilerConfig profiler_config(50 * core::Millisecond, 10 * core::Millisecond);
 
diff --git a/src/tests/roc_audio/test_resampler.cpp b/src/tests/roc_audio/test_resampler.cpp
index a48f11dee..f31248490 100644
--- a/src/tests/roc_audio/test_resampler.cpp
+++ b/src/tests/roc_audio/test_resampler.cpp
@@ -403,10 +403,10 @@ TEST(resampler, supported_scalings) {
                         const ResamplerBackend backend = supported_backends[n_back];
 
                         const SampleSpec in_spec =
-                            SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                            SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                        ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                         const SampleSpec out_spec =
-                            SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                            SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                        ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                         core::SharedPtr<IResampler> resampler =
@@ -460,10 +460,10 @@ TEST(resampler, invalid_scalings) {
                     const ResamplerBackend backend = supported_backends[n_back];
 
                     const SampleSpec in_spec =
-                        SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                     const SampleSpec out_spec =
-                        SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                     core::SharedPtr<IResampler> resampler = processor_map.new_resampler(
@@ -509,10 +509,10 @@ TEST(resampler, scaling_trend) {
                     const ResamplerBackend backend = supported_backends[n_back];
 
                     const SampleSpec in_spec =
-                        SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                     const SampleSpec out_spec =
-                        SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                     const float scaling = supported_scalings[n_scale];
@@ -587,7 +587,7 @@ TEST(resampler, upscale_downscale_mono) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         for (size_t n_dir = 0; n_dir < ROC_ARRAY_SIZE(supported_dirs); n_dir++) {
@@ -655,7 +655,7 @@ TEST(resampler, upscale_downscale_stereo) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         for (size_t n_dir = 0; n_dir < ROC_ARRAY_SIZE(supported_dirs); n_dir++) {
@@ -733,10 +733,10 @@ TEST(resampler, reader_timestamp_passthrough) {
                     const ResamplerBackend backend = supported_backends[n_back];
 
                     const SampleSpec in_spec =
-                        SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                     const SampleSpec out_spec =
-                        SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                     core::SharedPtr<IResampler> resampler = processor_map.new_resampler(
@@ -845,10 +845,10 @@ TEST(resampler, writer_timestamp_passthrough) {
                     const ResamplerBackend backend = supported_backends[n_back];
 
                     const SampleSpec in_spec =
-                        SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                     const SampleSpec out_spec =
-                        SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                     core::SharedPtr<IResampler> resampler = processor_map.new_resampler(
@@ -955,10 +955,10 @@ TEST(resampler, reader_timestamp_zero_or_small) {
                     const ResamplerBackend backend = supported_backends[n_back];
 
                     const SampleSpec in_spec =
-                        SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                     const SampleSpec out_spec =
-                        SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                     core::SharedPtr<IResampler> resampler = processor_map.new_resampler(
@@ -1033,10 +1033,10 @@ TEST(resampler, writer_timestamp_zero_or_small) {
                     const ResamplerBackend backend = supported_backends[n_back];
 
                     const SampleSpec in_spec =
-                        SampleSpec(supported_rates[n_irate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_irate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
                     const SampleSpec out_spec =
-                        SampleSpec(supported_rates[n_orate], Sample_RawFormat,
+                        SampleSpec(supported_rates[n_orate], PcmSubformat_Raw,
                                    ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
                     core::SharedPtr<IResampler> resampler = processor_map.new_resampler(
@@ -1098,7 +1098,7 @@ TEST(resampler, reader_big_frame) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         test::MockReader input_reader(frame_factory, sample_spec);
@@ -1137,7 +1137,7 @@ TEST(resampler, writer_big_frame) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         test::MockWriter output_writer;
@@ -1179,7 +1179,7 @@ TEST(resampler, reader_forward_mode) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         test::MockReader input_reader(frame_factory, sample_spec);
@@ -1224,7 +1224,7 @@ TEST(resampler, reader_forward_error) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         test::MockReader input_reader(frame_factory, sample_spec);
@@ -1268,7 +1268,7 @@ TEST(resampler, writer_forward_error) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         test::MockWriter output_writer;
@@ -1311,7 +1311,7 @@ TEST(resampler, reader_process_partial) {
         const ResamplerBackend backend = supported_backends[n_back];
         const ResamplerProfile profile = ResamplerProfile_High;
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChMask);
 
         test::MockReader input_reader(frame_factory, sample_spec);
@@ -1358,7 +1358,7 @@ TEST(resampler, reader_preallocated_buffer) {
             const ResamplerBackend backend = supported_backends[n_back];
             const ResamplerProfile profile = ResamplerProfile_High;
 
-            const SampleSpec sample_spec(SampleRate, Sample_RawFormat,
+            const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw,
                                          ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
             test::MockReader input_reader(frame_factory, sample_spec);
diff --git a/src/tests/roc_audio/test_sample_spec.cpp b/src/tests/roc_audio/test_sample_spec.cpp
index 893b2abf3..b2caa2587 100644
--- a/src/tests/roc_audio/test_sample_spec.cpp
+++ b/src/tests/roc_audio/test_sample_spec.cpp
@@ -10,9 +10,9 @@
 #include <CppUTest/UtestMacros.h>
 
 #include "roc_audio/channel_defs.h"
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_audio/sample.h"
-#include "roc_audio/sample_format.h"
 #include "roc_audio/sample_spec.cpp"
 #include "roc_core/cpu_traits.h"
 #include "roc_core/macro_helpers.h"
@@ -29,7 +29,7 @@ TEST(sample_spec, ns_2_nsamples) {
 
     for (size_t numChans = 1; numChans < ChanPos_Max; ++numChans) {
         const SampleSpec sample_spec =
-            SampleSpec((size_t)SampleRate, Sample_RawFormat, ChanLayout_Surround,
+            SampleSpec((size_t)SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                        ChanOrder_Smpte, ChannelMask(((uint64_t)1 << numChans) - 1));
 
         // num_channels
@@ -85,7 +85,7 @@ TEST(sample_spec, nsamples_2_ns) {
 
     for (size_t numChans = 1; numChans < ChanPos_Max; ++numChans) {
         const SampleSpec sample_spec =
-            SampleSpec((size_t)SampleRate, Sample_RawFormat, ChanLayout_Surround,
+            SampleSpec((size_t)SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                        ChanOrder_Smpte, ChannelMask(((uint64_t)1 << numChans) - 1));
 
         const core::nanoseconds_t sampling_period =
@@ -133,7 +133,7 @@ TEST(sample_spec, saturation) {
 #if ROC_CPU_BITS == 32
     {
         const SampleSpec sample_spec =
-            SampleSpec(1000000000, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+            SampleSpec(1000000000, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                        ChanMask_Surround_Stereo);
 
         // ns_2_samples_per_chan
@@ -143,7 +143,7 @@ TEST(sample_spec, saturation) {
 #endif
     {
         const SampleSpec sample_spec =
-            SampleSpec(1000000000, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+            SampleSpec(1000000000, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                        ChanMask_Surround_Stereo);
 
         // ns_2_samples_overall
@@ -154,7 +154,7 @@ TEST(sample_spec, saturation) {
 #if ROC_CPU_BITS == 64
     {
         const SampleSpec sample_spec =
-            SampleSpec(1, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+            SampleSpec(1, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                        ChanMask_Surround_Stereo);
 
         // samples_per_chan_2_ns
@@ -177,7 +177,7 @@ TEST(sample_spec, saturation) {
 #endif
     {
         const SampleSpec sample_spec =
-            SampleSpec(1, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte,
+            SampleSpec(1, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte,
                        ChanMask_Surround_Stereo);
 
         // ns_2_stream_timestamp_delta
@@ -197,7 +197,7 @@ TEST(sample_spec, bytes) {
         const size_t NumChans = 2;
         const size_t SampleSize = sizeof(sample_t);
 
-        const SampleSpec sample_spec(SampleRate, Sample_RawFormat, ChanLayout_Surround,
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_Raw, ChanLayout_Surround,
                                      ChanOrder_Smpte, ChanMask_Surround_Stereo);
 
         // bytes_2_stream_timestamp
@@ -221,8 +221,9 @@ TEST(sample_spec, bytes) {
         const size_t NumChans = 2;
         const size_t SampleSize = 3; // 24 bits
 
-        const SampleSpec sample_spec(SampleRate, PcmFormat_SInt24_Be, ChanLayout_Surround,
-                                     ChanOrder_Smpte, ChanMask_Surround_Stereo);
+        const SampleSpec sample_spec(SampleRate, PcmSubformat_SInt24_Be,
+                                     ChanLayout_Surround, ChanOrder_Smpte,
+                                     ChanMask_Surround_Stereo);
 
         // bytes_2_stream_timestamp
         CHECK_EQUAL(111,
@@ -242,31 +243,50 @@ TEST(sample_spec, bytes) {
     }
 }
 
-TEST(sample_spec, clear) {
+TEST(sample_spec, empty_complete) {
     SampleSpec sample_spec;
 
-    // sample spec is invalid
-    CHECK(!sample_spec.is_valid());
+    // sample spec is empty
+    CHECK(!sample_spec.is_complete());
+    CHECK(sample_spec.is_empty());
     CHECK_EQUAL(0, sample_spec.sample_rate());
-    CHECK_EQUAL(SampleFormat_Invalid, sample_spec.sample_format());
-    CHECK_EQUAL(PcmFormat_Invalid, sample_spec.pcm_format());
+    CHECK_EQUAL(Format_Invalid, sample_spec.format());
+    CHECK_EQUAL(PcmSubformat_Invalid, sample_spec.pcm_subformat());
     CHECK_EQUAL(ChanLayout_None, sample_spec.channel_set().layout());
     CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
     CHECK_EQUAL(0, sample_spec.channel_set().num_channels());
 
     // set all fields
     sample_spec.set_sample_rate(44100);
-    sample_spec.set_sample_format(SampleFormat_Pcm);
-    sample_spec.set_pcm_format(PcmFormat_Float32);
+    CHECK(!sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
+
+    sample_spec.set_format(Format_Pcm);
+    CHECK(!sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
+
+    sample_spec.set_pcm_subformat(PcmSubformat_Float32);
+    CHECK(!sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
+
     sample_spec.channel_set().set_layout(ChanLayout_Surround);
+    CHECK(!sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
+
     sample_spec.channel_set().set_order(ChanOrder_Smpte);
+    CHECK(!sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
+
     sample_spec.channel_set().set_mask(ChanMask_Surround_Stereo);
+    CHECK(sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
 
-    // sample spec is valid
-    CHECK(sample_spec.is_valid());
+    // sample spec is complete
+    CHECK(sample_spec.is_complete());
+    CHECK(!sample_spec.is_empty());
     CHECK_EQUAL(44100, sample_spec.sample_rate());
-    CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-    CHECK_EQUAL(PcmFormat_Float32, sample_spec.pcm_format());
+    CHECK_EQUAL(Format_Pcm, sample_spec.format());
+    CHECK_EQUAL(PcmSubformat_Float32, sample_spec.pcm_subformat());
     CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
     CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
     CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
@@ -274,47 +294,68 @@ TEST(sample_spec, clear) {
     // clear all fields
     sample_spec.clear();
 
-    // sample spec is invalid
-    CHECK(!sample_spec.is_valid());
+    // sample spec is empty
+    CHECK(!sample_spec.is_complete());
+    CHECK(sample_spec.is_empty());
     CHECK_EQUAL(0, sample_spec.sample_rate());
-    CHECK_EQUAL(SampleFormat_Invalid, sample_spec.sample_format());
-    CHECK_EQUAL(PcmFormat_Invalid, sample_spec.pcm_format());
+    CHECK_EQUAL(Format_Invalid, sample_spec.format());
+    CHECK_EQUAL(PcmSubformat_Invalid, sample_spec.pcm_subformat());
     CHECK_EQUAL(ChanLayout_None, sample_spec.channel_set().layout());
     CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
     CHECK_EQUAL(0, sample_spec.channel_set().num_channels());
 }
 
-TEST(sample_spec, is_raw) {
+TEST(sample_spec, pcm_raw) {
     { // empty
         SampleSpec sample_spec;
-        CHECK(!sample_spec.is_valid());
+        CHECK(!sample_spec.is_complete());
+        CHECK(sample_spec.is_empty());
+        CHECK(!sample_spec.is_pcm());
         CHECK(!sample_spec.is_raw());
     }
-    { // incomplete
+    { // incomplete (pcm, raw)
         SampleSpec sample_spec;
-        sample_spec.set_sample_format(SampleFormat_Pcm);
-        sample_spec.set_pcm_format(PcmFormat_Float32);
-        CHECK(!sample_spec.is_valid());
+        sample_spec.set_format(Format_Pcm);
+        sample_spec.set_pcm_subformat(PcmSubformat_Float32);
+        CHECK(!sample_spec.is_complete());
+        CHECK(!sample_spec.is_empty());
+        CHECK(sample_spec.is_pcm());
         CHECK(sample_spec.is_raw());
     }
-    { // complete
+    { // complete (pcm, raw)
         SampleSpec sample_spec;
-        sample_spec.set_sample_format(SampleFormat_Pcm);
-        sample_spec.set_pcm_format(PcmFormat_Float32);
+        sample_spec.set_format(Format_Pcm);
+        sample_spec.set_pcm_subformat(PcmSubformat_Float32);
         sample_spec.set_sample_rate(44100);
         sample_spec.set_channel_set(
             ChannelSet(ChanLayout_Surround, ChanOrder_Smpte, ChanMask_Surround_Stereo));
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
+        CHECK(!sample_spec.is_empty());
+        CHECK(sample_spec.is_pcm());
         CHECK(sample_spec.is_raw());
     }
-    { // pcm format mismatch
+    { // format mismatch (non-pcm)
+        SampleSpec sample_spec;
+        sample_spec.set_format(Format_Wav);
+        sample_spec.set_pcm_subformat(PcmSubformat_Float32);
+        sample_spec.set_sample_rate(44100);
+        sample_spec.set_channel_set(
+            ChannelSet(ChanLayout_Surround, ChanOrder_Smpte, ChanMask_Surround_Stereo));
+        CHECK(sample_spec.is_complete());
+        CHECK(!sample_spec.is_empty());
+        CHECK(!sample_spec.is_pcm());
+        CHECK(!sample_spec.is_raw());
+    }
+    { // sub-format mismatch (pcm, non-raw)
         SampleSpec sample_spec;
-        sample_spec.set_sample_format(SampleFormat_Pcm);
-        sample_spec.set_pcm_format(PcmFormat_Float32_Be);
+        sample_spec.set_format(Format_Pcm);
+        sample_spec.set_pcm_subformat(PcmSubformat_Float32_Be);
         sample_spec.set_sample_rate(44100);
         sample_spec.set_channel_set(
             ChannelSet(ChanLayout_Surround, ChanOrder_Smpte, ChanMask_Surround_Stereo));
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
+        CHECK(!sample_spec.is_empty());
+        CHECK(sample_spec.is_pcm());
         CHECK(!sample_spec.is_raw());
     }
 }
@@ -322,26 +363,26 @@ TEST(sample_spec, is_raw) {
 TEST(sample_spec, parse_rate) {
     { // 44.1Khz
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/44100/stereo", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/44100/stereo", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(44100, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
     }
     { // 48Khz
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/stereo", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/stereo", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
@@ -349,83 +390,123 @@ TEST(sample_spec, parse_rate) {
 }
 
 TEST(sample_spec, parse_format) {
-    { // uint8
+    { // format: pcm, subformat: uint8
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("u8/44100/stereo", sample_spec));
+        CHECK(parse_sample_spec("pcm@u8/44100/stereo", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(44100, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_UInt8, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_UInt8, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+
+        STRCMP_EQUAL("pcm", sample_spec.format_name());
+        STRCMP_EQUAL("u8", sample_spec.subformat_name());
     }
-    { // sint18_4_le
+    { // format: pcm, subformat: sint18_4_le
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s18_4le/48000/stereo", sample_spec));
+        CHECK(parse_sample_spec("pcm@s18_4le/48000/stereo", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt18_4_Le, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt18_4_Le, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+
+        STRCMP_EQUAL("pcm", sample_spec.format_name());
+        STRCMP_EQUAL("s18_4le", sample_spec.subformat_name());
     }
-    { // float32
+    { // format: wav, subformat: float32
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("f32/48000/stereo", sample_spec));
+        CHECK(parse_sample_spec("wav@f32/48000/stereo", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_Float32, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Wav, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_Float32, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+
+        STRCMP_EQUAL("wav", sample_spec.format_name());
+        STRCMP_EQUAL("f32", sample_spec.subformat_name());
+    }
+    { // format: custom, subformat: float64
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("test@f64/48000/stereo", sample_spec));
+
+        CHECK(sample_spec.is_complete());
+
+        CHECK_EQUAL(48000, sample_spec.sample_rate());
+        CHECK_EQUAL(Format_Custom, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_Float64, sample_spec.pcm_subformat());
+        CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
+        CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
+        CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+
+        STRCMP_EQUAL("test", sample_spec.format_name());
+        STRCMP_EQUAL("f64", sample_spec.subformat_name());
+    }
+    { // format: custom, subformat: custom
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("xxx@yyy/48000/stereo", sample_spec));
+
+        CHECK(sample_spec.is_complete());
+
+        CHECK_EQUAL(48000, sample_spec.sample_rate());
+        CHECK_EQUAL(Format_Custom, sample_spec.format());
+        CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
+        CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
+        CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+
+        STRCMP_EQUAL("xxx", sample_spec.format_name());
+        STRCMP_EQUAL("yyy", sample_spec.subformat_name());
     }
 }
 
 IGNORE_TEST(sample_spec, parse_channels) {
     { // surround stereo
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/stereo", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/stereo", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
     }
     { // surround 5.1.2
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/surround5.1.2", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/surround5.1.2", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_5_1_2));
     }
     { // surround channel list
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/FL,FC,FR", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/FL,FC,FR", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
 
@@ -436,13 +517,13 @@ IGNORE_TEST(sample_spec, parse_channels) {
     }
     { // multitrack channel list
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/1,2,3", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/1,2,3", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Multitrack, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
 
@@ -453,13 +534,13 @@ IGNORE_TEST(sample_spec, parse_channels) {
     }
     { // multitrack channel range
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/1-3", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/1-3", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Multitrack, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
 
@@ -470,13 +551,13 @@ IGNORE_TEST(sample_spec, parse_channels) {
     }
     { // multitrack channel list and range
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/1,3-5,7", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/1,3-5,7", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Multitrack, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
 
@@ -489,13 +570,13 @@ IGNORE_TEST(sample_spec, parse_channels) {
     }
     { // multitrack mask (zero)
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/0x00", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/0x00", sample_spec));
 
-        CHECK(!sample_spec.is_valid());
+        CHECK(!sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Multitrack, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
         CHECK_EQUAL(0, sample_spec.num_channels());
@@ -503,13 +584,13 @@ IGNORE_TEST(sample_spec, parse_channels) {
     { // multitrack mask (short)
         SampleSpec sample_spec;
         // 0xAC = 10101100
-        CHECK(parse_sample_spec("s16/48000/0xAC", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/0xAC", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Multitrack, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
 
@@ -522,13 +603,13 @@ IGNORE_TEST(sample_spec, parse_channels) {
     { // multitrack mask (long)
         SampleSpec sample_spec;
         // 1010, 80 zero bits, 1100
-        CHECK(parse_sample_spec("s16/48000/0xA00000000000000000000C", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/0xA00000000000000000000C", sample_spec));
 
-        CHECK(sample_spec.is_valid());
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Multitrack, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
 
@@ -545,37 +626,63 @@ TEST(sample_spec, parse_defaults) {
         SampleSpec sample_spec;
         CHECK(parse_sample_spec("-/44100/stereo", sample_spec));
 
-        CHECK(!sample_spec.is_valid());
+        CHECK(!sample_spec.is_complete());
+
+        CHECK_EQUAL(44100, sample_spec.sample_rate());
+        CHECK_EQUAL(Format_Invalid, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_Invalid, sample_spec.pcm_subformat());
+        CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
+        CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
+        CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+    }
+    { // no subformat (wav format)
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("wav/44100/stereo", sample_spec));
+
+        CHECK(sample_spec.is_complete());
+
+        CHECK_EQUAL(44100, sample_spec.sample_rate());
+        CHECK_EQUAL(Format_Wav, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_Invalid, sample_spec.pcm_subformat());
+        CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
+        CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
+        CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
+    }
+    { // no subformat (custom format)
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("test/44100/stereo", sample_spec));
+
+        CHECK(sample_spec.is_complete());
 
         CHECK_EQUAL(44100, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Invalid, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_Invalid, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Custom, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_Invalid, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
     }
     { // no rate
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/-/stereo", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/-/stereo", sample_spec));
 
-        CHECK(!sample_spec.is_valid());
+        CHECK(!sample_spec.is_complete());
 
         CHECK_EQUAL(0, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_Surround, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_Smpte, sample_spec.channel_set().order());
         CHECK(sample_spec.channel_set().is_equal(ChanMask_Surround_Stereo));
     }
     { // no channels
         SampleSpec sample_spec;
-        CHECK(parse_sample_spec("s16/48000/-", sample_spec));
+        CHECK(parse_sample_spec("pcm@s16/48000/-", sample_spec));
 
-        CHECK(!sample_spec.is_valid());
+        CHECK(!sample_spec.is_complete());
 
         CHECK_EQUAL(48000, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Pcm, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_SInt16, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Pcm, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_SInt16, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_None, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
         CHECK_EQUAL(0, sample_spec.channel_set().num_channels());
@@ -584,17 +691,65 @@ TEST(sample_spec, parse_defaults) {
         SampleSpec sample_spec;
         CHECK(parse_sample_spec("-/-/-", sample_spec));
 
-        CHECK(!sample_spec.is_valid());
+        CHECK(!sample_spec.is_complete());
 
         CHECK_EQUAL(0, sample_spec.sample_rate());
-        CHECK_EQUAL(SampleFormat_Invalid, sample_spec.sample_format());
-        CHECK_EQUAL(PcmFormat_Invalid, sample_spec.pcm_format());
+        CHECK_EQUAL(Format_Invalid, sample_spec.format());
+        CHECK_EQUAL(PcmSubformat_Invalid, sample_spec.pcm_subformat());
         CHECK_EQUAL(ChanLayout_None, sample_spec.channel_set().layout());
         CHECK_EQUAL(ChanOrder_None, sample_spec.channel_set().order());
         CHECK_EQUAL(0, sample_spec.channel_set().num_channels());
     }
 }
 
+TEST(sample_spec, has_field) {
+    { // all fields
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("pcm@s16/44100/stereo", sample_spec));
+
+        CHECK(sample_spec.has_format());
+        CHECK(sample_spec.has_subformat());
+        CHECK(sample_spec.has_sample_rate());
+        CHECK(sample_spec.has_channel_set());
+    }
+    { // no sub-format
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("wav/44100/stereo", sample_spec));
+
+        CHECK(sample_spec.has_format());
+        CHECK(!sample_spec.has_subformat());
+        CHECK(sample_spec.has_sample_rate());
+        CHECK(sample_spec.has_channel_set());
+    }
+    { // no rate
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("pcm@s16/-/stereo", sample_spec));
+
+        CHECK(sample_spec.has_format());
+        CHECK(sample_spec.has_subformat());
+        CHECK(!sample_spec.has_sample_rate());
+        CHECK(sample_spec.has_channel_set());
+    }
+    { // no channels
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("pcm@s16/44100/-", sample_spec));
+
+        CHECK(sample_spec.has_format());
+        CHECK(sample_spec.has_subformat());
+        CHECK(sample_spec.has_sample_rate());
+        CHECK(!sample_spec.has_channel_set());
+    }
+    { // no fields
+        SampleSpec sample_spec;
+        CHECK(parse_sample_spec("-/-/-", sample_spec));
+
+        CHECK(!sample_spec.has_format());
+        CHECK(!sample_spec.has_subformat());
+        CHECK(!sample_spec.has_sample_rate());
+        CHECK(!sample_spec.has_channel_set());
+    }
+}
+
 TEST(sample_spec, parse_errors) {
     SampleSpec sample_spec;
 
@@ -604,40 +759,50 @@ TEST(sample_spec, parse_errors) {
         CHECK(!parse_sample_spec("//", sample_spec));
         CHECK(!parse_sample_spec("///", sample_spec));
         CHECK(!parse_sample_spec("/48000/stereo", sample_spec));
-        CHECK(!parse_sample_spec("s16//stereo", sample_spec));
-        CHECK(!parse_sample_spec("s16/48000/", sample_spec));
-        CHECK(!parse_sample_spec("/s16/48000/stereo", sample_spec));
-        CHECK(!parse_sample_spec("s16/48000/stereo/", sample_spec));
+        CHECK(!parse_sample_spec("pcm@/48000/stereo", sample_spec));
+        CHECK(!parse_sample_spec("@s16/48000/stereo", sample_spec));
+        CHECK(!parse_sample_spec("@/48000/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16//stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/48000/", sample_spec));
+        CHECK(!parse_sample_spec("/pcm@s16/48000/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/48000/stereo/", sample_spec));
+        CHECK(!parse_sample_spec("pcm@/48000/stereo", sample_spec));
+        CHECK(!parse_sample_spec("@s16/48000/stereo", sample_spec));
     }
     { // bad rate
-        CHECK(!parse_sample_spec("s16/0/stereo", sample_spec));
-        CHECK(!parse_sample_spec("s16/-1/stereo", sample_spec));
-        CHECK(!parse_sample_spec("s16/bad/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/0/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/-1/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/bad/stereo", sample_spec));
     }
     { // bad format
-        CHECK(!parse_sample_spec("s77/44100/stereo", sample_spec));
-        CHECK(!parse_sample_spec("xxx/44100/stereo", sample_spec));
+        CHECK(!parse_sample_spec("!!!@s16/44100/stereo", sample_spec));
+        CHECK(!parse_sample_spec("!!!/44100/stereo", sample_spec));
+    }
+    { // bad subformat
+        CHECK(!parse_sample_spec("pcm@s77/44100/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm@xxx/44100/stereo", sample_spec));
+        CHECK(!parse_sample_spec("pcm/44100/stereo", sample_spec));
     }
     { // bad surround
-        CHECK(!parse_sample_spec("s16/44100/bad", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/BAD,BAD", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/stereo,", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/FL,FR,", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/,FL,FR", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/FL,,FR", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/bad", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/BAD,BAD", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/stereo,", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/FL,FR,", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/,FL,FR", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/FL,,FR", sample_spec));
     }
     { // bad multitrack
-        CHECK(!parse_sample_spec("s16/44100/1,2,", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/,1,2", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/1,,2", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/1-", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/-2", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/1--2", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/10000", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/10000-20000", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/0x", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/0XF", sample_spec));
-        CHECK(!parse_sample_spec("s16/44100/0xZZ", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/1,2,", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/,1,2", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/1,,2", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/1-", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/-2", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/1--2", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/10000", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/10000-20000", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/0x", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/0XF", sample_spec));
+        CHECK(!parse_sample_spec("pcm@s16/44100/0xZZ", sample_spec));
     }
 }
 
diff --git a/src/tests/roc_audio/test_samples/generate_samples.py b/src/tests/roc_audio/test_samples/generate_samples.py
index d23626d3b..5f6842bf3 100755
--- a/src/tests/roc_audio/test_samples/generate_samples.py
+++ b/src/tests/roc_audio/test_samples/generate_samples.py
@@ -4,7 +4,7 @@
 import subprocess
 
 def format_name(encoding, endian):
-    return 'PcmFormat_' + \
+    return 'PcmSubformat_' + \
         encoding['pcm_encoding'] + '_' + endian['pcm_endian']
 
 def format_array(array, maxlen=8, indent=1):
diff --git a/src/tests/roc_audio/test_samples/pcm_float32_be.h b/src/tests/roc_audio/test_samples/pcm_float32_be.h
index 92cc19d89..66881163e 100644
--- a/src/tests/roc_audio/test_samples/pcm_float32_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_float32_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_float32_be = {
   /* name */ "pcm_float32_be",
 
-  /* format */ PcmFormat_Float32_Be,
+  /* format */ PcmSubformat_Float32_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_float32_le.h b/src/tests/roc_audio/test_samples/pcm_float32_le.h
index b02b75231..28ac25597 100644
--- a/src/tests/roc_audio/test_samples/pcm_float32_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_float32_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_float32_le = {
   /* name */ "pcm_float32_le",
 
-  /* format */ PcmFormat_Float32_Le,
+  /* format */ PcmSubformat_Float32_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint16_be.h b/src/tests/roc_audio/test_samples/pcm_sint16_be.h
index 1963e0c55..86d6cf0de 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint16_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint16_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint16_be = {
   /* name */ "pcm_sint16_be",
 
-  /* format */ PcmFormat_SInt16_Be,
+  /* format */ PcmSubformat_SInt16_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint16_le.h b/src/tests/roc_audio/test_samples/pcm_sint16_le.h
index 50d20e0ed..ddb07462c 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint16_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint16_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint16_le = {
   /* name */ "pcm_sint16_le",
 
-  /* format */ PcmFormat_SInt16_Le,
+  /* format */ PcmSubformat_SInt16_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint24_be.h b/src/tests/roc_audio/test_samples/pcm_sint24_be.h
index e0302f5a1..3c4f85080 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint24_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint24_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint24_be = {
   /* name */ "pcm_sint24_be",
 
-  /* format */ PcmFormat_SInt24_Be,
+  /* format */ PcmSubformat_SInt24_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint24_le.h b/src/tests/roc_audio/test_samples/pcm_sint24_le.h
index 2b3d74b50..8e82b81cb 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint24_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint24_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint24_le = {
   /* name */ "pcm_sint24_le",
 
-  /* format */ PcmFormat_SInt24_Le,
+  /* format */ PcmSubformat_SInt24_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint32_be.h b/src/tests/roc_audio/test_samples/pcm_sint32_be.h
index 9aac503cd..4c6499421 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint32_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint32_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint32_be = {
   /* name */ "pcm_sint32_be",
 
-  /* format */ PcmFormat_SInt32_Be,
+  /* format */ PcmSubformat_SInt32_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint32_le.h b/src/tests/roc_audio/test_samples/pcm_sint32_le.h
index 59cd59eb1..2a0227f8b 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint32_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint32_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint32_le = {
   /* name */ "pcm_sint32_le",
 
-  /* format */ PcmFormat_SInt32_Le,
+  /* format */ PcmSubformat_SInt32_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint8_be.h b/src/tests/roc_audio/test_samples/pcm_sint8_be.h
index e6ebde2bb..f7d8c8e9c 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint8_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint8_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint8_be = {
   /* name */ "pcm_sint8_be",
 
-  /* format */ PcmFormat_SInt8_Be,
+  /* format */ PcmSubformat_SInt8_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_sint8_le.h b/src/tests/roc_audio/test_samples/pcm_sint8_le.h
index a34c6a063..2393a41cc 100644
--- a/src/tests/roc_audio/test_samples/pcm_sint8_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_sint8_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_sint8_le = {
   /* name */ "pcm_sint8_le",
 
-  /* format */ PcmFormat_SInt8_Le,
+  /* format */ PcmSubformat_SInt8_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint16_be.h b/src/tests/roc_audio/test_samples/pcm_uint16_be.h
index 349a5da65..7aba88274 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint16_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint16_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint16_be = {
   /* name */ "pcm_uint16_be",
 
-  /* format */ PcmFormat_UInt16_Be,
+  /* format */ PcmSubformat_UInt16_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint16_le.h b/src/tests/roc_audio/test_samples/pcm_uint16_le.h
index 612fb080e..e419214da 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint16_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint16_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint16_le = {
   /* name */ "pcm_uint16_le",
 
-  /* format */ PcmFormat_UInt16_Le,
+  /* format */ PcmSubformat_UInt16_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint24_be.h b/src/tests/roc_audio/test_samples/pcm_uint24_be.h
index 9f9d82505..b9af31eaf 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint24_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint24_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint24_be = {
   /* name */ "pcm_uint24_be",
 
-  /* format */ PcmFormat_UInt24_Be,
+  /* format */ PcmSubformat_UInt24_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint24_le.h b/src/tests/roc_audio/test_samples/pcm_uint24_le.h
index ffb82bc60..a96343bae 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint24_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint24_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint24_le = {
   /* name */ "pcm_uint24_le",
 
-  /* format */ PcmFormat_UInt24_Le,
+  /* format */ PcmSubformat_UInt24_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint32_be.h b/src/tests/roc_audio/test_samples/pcm_uint32_be.h
index 4fc6e9354..c4cc020fd 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint32_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint32_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint32_be = {
   /* name */ "pcm_uint32_be",
 
-  /* format */ PcmFormat_UInt32_Be,
+  /* format */ PcmSubformat_UInt32_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint32_le.h b/src/tests/roc_audio/test_samples/pcm_uint32_le.h
index afa25310b..7c78b5f50 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint32_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint32_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint32_le = {
   /* name */ "pcm_uint32_le",
 
-  /* format */ PcmFormat_UInt32_Le,
+  /* format */ PcmSubformat_UInt32_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint8_be.h b/src/tests/roc_audio/test_samples/pcm_uint8_be.h
index 6dddf94fe..5247a53e0 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint8_be.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint8_be.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint8_be = {
   /* name */ "pcm_uint8_be",
 
-  /* format */ PcmFormat_UInt8_Be,
+  /* format */ PcmSubformat_UInt8_Be,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/pcm_uint8_le.h b/src/tests/roc_audio/test_samples/pcm_uint8_le.h
index 16a515b09..baa56af9f 100644
--- a/src/tests/roc_audio/test_samples/pcm_uint8_le.h
+++ b/src/tests/roc_audio/test_samples/pcm_uint8_le.h
@@ -14,7 +14,7 @@ namespace test {
 static SampleInfo sample_pcm_uint8_le = {
   /* name */ "pcm_uint8_le",
 
-  /* format */ PcmFormat_UInt8_Le,
+  /* format */ PcmSubformat_UInt8_Le,
 
   /* num_samples */ 240,
   /* samples     */ {
diff --git a/src/tests/roc_audio/test_samples/sample_info.h b/src/tests/roc_audio/test_samples/sample_info.h
index fda8150c1..5ec2be167 100644
--- a/src/tests/roc_audio/test_samples/sample_info.h
+++ b/src/tests/roc_audio/test_samples/sample_info.h
@@ -9,7 +9,7 @@
 #ifndef ROC_AUDIO_TEST_SAMPLES_SAMPLE_INFO_H_
 #define ROC_AUDIO_TEST_SAMPLES_SAMPLE_INFO_H_
 
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/stddefs.h"
 
 namespace roc {
@@ -21,7 +21,7 @@ struct SampleInfo {
 
     const char* name;
 
-    PcmFormat format;
+    PcmSubformat format;
 
     size_t num_samples;
     float samples[MaxSamples];
diff --git a/src/tests/roc_audio/test_watchdog.cpp b/src/tests/roc_audio/test_watchdog.cpp
index acc192603..39afffddd 100644
--- a/src/tests/roc_audio/test_watchdog.cpp
+++ b/src/tests/roc_audio/test_watchdog.cpp
@@ -36,7 +36,7 @@ enum {
 const sample_t magic_sample = 42;
 
 const SampleSpec sample_spec(
-    SampleRate, Sample_RawFormat, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
+    SampleRate, PcmSubformat_Raw, ChanLayout_Surround, ChanOrder_Smpte, ChMask);
 
 core::HeapArena arena;
 FrameFactory frame_factory(arena, MaxBufSize * sizeof(sample_t));
diff --git a/src/tests/roc_core/test_string_list.cpp b/src/tests/roc_core/test_string_list.cpp
index a006e14b7..fc97d806d 100644
--- a/src/tests/roc_core/test_string_list.cpp
+++ b/src/tests/roc_core/test_string_list.cpp
@@ -255,15 +255,15 @@ TEST(string_list, exponential_growth) {
     int num_reallocs = 0;
 
     int expected_reallocs[] = {
-        1, 1, 1,                               //
-        2, 2, 2,                               //
-        3, 3, 3, 3, 3, 3,                      //
-        4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, //
-        5, 5, 5, 5, 5, 5, 5                    //
+        1, 1,                            //
+        2, 2, 2,                         //
+        3, 3, 3, 3, 3,                   //
+        4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, //
+        5, 5, 5, 5, 5                    //
     };
 
     for (size_t n = 0; n < ROC_ARRAY_SIZE(expected_reallocs); n++) {
-        CHECK(sl.push_back("123456789abcdef,123456789abcdef"));
+        CHECK(sl.push_back("123456789abcd,123456789abcd"));
 
         if (prev_front != sl.front()) {
             num_reallocs++;
diff --git a/src/tests/roc_packet/test_delayed_reader.cpp b/src/tests/roc_packet/test_delayed_reader.cpp
index 2dbb57203..d566ee98f 100644
--- a/src/tests/roc_packet/test_delayed_reader.cpp
+++ b/src/tests/roc_packet/test_delayed_reader.cpp
@@ -25,7 +25,7 @@ enum { SampleRate = 1000, NumSamples = 100, NumPackets = 30, MaxBufSize = 100 };
 const core::nanoseconds_t NsPerSample = core::Second / SampleRate;
 
 const audio::SampleSpec sample_spec(SampleRate,
-                                    audio::Sample_RawFormat,
+                                    audio::PcmSubformat_Raw,
                                     audio::ChanLayout_Surround,
                                     audio::ChanOrder_Smpte,
                                     audio::ChanMask_Surround_Stereo);
diff --git a/src/tests/roc_pipeline/bench_pipeline_loop_contention.cpp b/src/tests/roc_pipeline/bench_pipeline_loop_contention.cpp
index 793e9f0f1..9b9036da3 100644
--- a/src/tests/roc_pipeline/bench_pipeline_loop_contention.cpp
+++ b/src/tests/roc_pipeline/bench_pipeline_loop_contention.cpp
@@ -55,7 +55,7 @@ class NoopPipeline : public PipelineLoop,
         : PipelineLoop(*this,
                        config,
                        audio::SampleSpec(SampleRate,
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          audio::ChanLayout_Surround,
                                          audio::ChanOrder_Smpte,
                                          Chans),
diff --git a/src/tests/roc_pipeline/bench_pipeline_loop_peak_load.cpp b/src/tests/roc_pipeline/bench_pipeline_loop_peak_load.cpp
index e685d663e..6495782d8 100644
--- a/src/tests/roc_pipeline/bench_pipeline_loop_peak_load.cpp
+++ b/src/tests/roc_pipeline/bench_pipeline_loop_peak_load.cpp
@@ -279,7 +279,7 @@ class TestPipeline : public PipelineLoop,
         : PipelineLoop(*this,
                        config,
                        audio::SampleSpec(SampleRate,
-                                         audio::Sample_RawFormat,
+                                         audio::PcmSubformat_Raw,
                                          audio::ChanLayout_Surround,
                                          audio::ChanOrder_Smpte,
                                          Chans),
diff --git a/src/tests/roc_pipeline/test_helpers/mock_sink.h b/src/tests/roc_pipeline/test_helpers/mock_sink.h
index f7206cd91..aa0fcec5b 100644
--- a/src/tests/roc_pipeline/test_helpers/mock_sink.h
+++ b/src/tests/roc_pipeline/test_helpers/mock_sink.h
@@ -47,6 +47,10 @@ class MockSink : public sndio::ISink, public core::NonCopyable<> {
         return audio::SampleSpec();
     }
 
+    core::nanoseconds_t frame_length() const {
+        return 0;
+    }
+
     virtual bool has_state() const {
         return false;
     }
diff --git a/src/tests/roc_pipeline/test_helpers/mock_source.h b/src/tests/roc_pipeline/test_helpers/mock_source.h
index c145faef8..65c42207e 100644
--- a/src/tests/roc_pipeline/test_helpers/mock_source.h
+++ b/src/tests/roc_pipeline/test_helpers/mock_source.h
@@ -53,6 +53,10 @@ class MockSource : public sndio::ISource {
         return audio::SampleSpec();
     }
 
+    core::nanoseconds_t frame_length() const {
+        return 0;
+    }
+
     virtual bool has_state() const {
         return true;
     }
diff --git a/src/tests/roc_pipeline/test_loopback_sink_2_source.cpp b/src/tests/roc_pipeline/test_loopback_sink_2_source.cpp
index a0587279e..4c47930ec 100644
--- a/src/tests/roc_pipeline/test_loopback_sink_2_source.cpp
+++ b/src/tests/roc_pipeline/test_loopback_sink_2_source.cpp
@@ -50,10 +50,10 @@ namespace {
 const audio::ChannelMask Chans_Mono = audio::ChanMask_Surround_Mono;
 const audio::ChannelMask Chans_Stereo = audio::ChanMask_Surround_Stereo;
 
-const audio::PcmFormat Format_Raw = audio::Sample_RawFormat;
-const audio::PcmFormat Format_S16_Be = audio::PcmFormat_SInt16_Be;
-const audio::PcmFormat Format_S16_Ne = audio::PcmFormat_SInt16;
-const audio::PcmFormat Format_S32_Ne = audio::PcmFormat_SInt32;
+const audio::PcmSubformat Format_Raw = audio::PcmSubformat_Raw;
+const audio::PcmSubformat Format_S16_Be = audio::PcmSubformat_SInt16_Be;
+const audio::PcmSubformat Format_S16_Ne = audio::PcmSubformat_SInt16;
+const audio::PcmSubformat Format_S32_Ne = audio::PcmSubformat_SInt32;
 
 const rtp::PayloadType PayloadType_Ch1 = rtp::PayloadType_L16_Mono;
 const rtp::PayloadType PayloadType_Ch2 = rtp::PayloadType_L16_Stereo;
@@ -244,14 +244,14 @@ class PacketProxy : core::NonCopyable<> {
 };
 
 SenderSinkConfig make_sender_config(int flags,
-                                    audio::PcmFormat frame_format,
+                                    audio::PcmSubformat frame_format,
                                     audio::ChannelMask frame_channels,
                                     audio::ChannelMask packet_channels) {
     SenderSinkConfig config;
 
+    config.input_sample_spec.set_format(audio::Format_Pcm);
+    config.input_sample_spec.set_pcm_subformat(frame_format);
     config.input_sample_spec.set_sample_rate(SampleRate);
-    config.input_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-    config.input_sample_spec.set_pcm_format(frame_format);
     config.input_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
     config.input_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
     config.input_sample_spec.channel_set().set_mask(frame_channels);
@@ -291,14 +291,14 @@ SenderSinkConfig make_sender_config(int flags,
     return config;
 }
 
-ReceiverSourceConfig make_receiver_config(audio::PcmFormat frame_format,
+ReceiverSourceConfig make_receiver_config(audio::PcmSubformat frame_format,
                                           audio::ChannelMask frame_channels,
                                           audio::ChannelMask packet_channels) {
     ReceiverSourceConfig config;
 
+    config.common.output_sample_spec.set_format(audio::Format_Pcm);
+    config.common.output_sample_spec.set_pcm_subformat(frame_format);
     config.common.output_sample_spec.set_sample_rate(SampleRate);
-    config.common.output_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-    config.common.output_sample_spec.set_pcm_format(frame_format);
     config.common.output_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
     config.common.output_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
     config.common.output_sample_spec.channel_set().set_mask(frame_channels);
@@ -356,7 +356,7 @@ bool is_fec_supported(int flags) {
 
 void write_samples(test::FrameWriter& frame_writer,
                    size_t n_samples,
-                   audio::PcmFormat frame_format,
+                   audio::PcmSubformat frame_format,
                    const audio::SampleSpec& sample_spec,
                    core::nanoseconds_t base_cts) {
     if (frame_format == Format_Raw) {
@@ -373,7 +373,7 @@ void write_samples(test::FrameWriter& frame_writer,
 void read_samples(test::FrameReader& frame_reader,
                   size_t n_samples,
                   size_t n_sessions,
-                  audio::PcmFormat frame_format,
+                  audio::PcmSubformat frame_format,
                   const audio::SampleSpec& sample_spec,
                   core::nanoseconds_t base_cts) {
     if (frame_format == Format_Raw) {
@@ -473,7 +473,7 @@ void check_metrics(ReceiverSlot& receiver,
 
 void send_receive(int flags,
                   size_t num_sessions,
-                  audio::PcmFormat frame_format,
+                  audio::PcmSubformat frame_format,
                   audio::ChannelMask frame_channels,
                   audio::ChannelMask packet_channels) {
     packet::FifoQueue sender_outbound_queue;
diff --git a/src/tests/roc_pipeline/test_pipeline_loop.cpp b/src/tests/roc_pipeline/test_pipeline_loop.cpp
index f20fad2dd..3a070a0c5 100644
--- a/src/tests/roc_pipeline/test_pipeline_loop.cpp
+++ b/src/tests/roc_pipeline/test_pipeline_loop.cpp
@@ -59,7 +59,7 @@ core::SlabPool<core::Buffer> big_frame_buffer_pool("big_frame_buffer_pool",
 audio::FrameFactory big_frame_factory(frame_pool, big_frame_buffer_pool);
 
 const audio::SampleSpec sample_spec(SampleRate,
-                                    audio::Sample_RawFormat,
+                                    audio::PcmSubformat_Raw,
                                     audio::ChanLayout_Surround,
                                     audio::ChanOrder_Smpte,
                                     Chans);
diff --git a/src/tests/roc_pipeline/test_receiver_source.cpp b/src/tests/roc_pipeline/test_receiver_source.cpp
index 784c6a894..5186fa901 100644
--- a/src/tests/roc_pipeline/test_receiver_source.cpp
+++ b/src/tests/roc_pipeline/test_receiver_source.cpp
@@ -48,10 +48,10 @@ namespace {
 const audio::ChannelMask Chans_Mono = audio::ChanMask_Surround_Mono;
 const audio::ChannelMask Chans_Stereo = audio::ChanMask_Surround_Stereo;
 
-const audio::PcmFormat Format_Raw = audio::Sample_RawFormat;
-const audio::PcmFormat Format_S16_Be = audio::PcmFormat_SInt16_Be;
-const audio::PcmFormat Format_S16_Ne = audio::PcmFormat_SInt16;
-const audio::PcmFormat Format_S32_Ne = audio::PcmFormat_SInt32;
+const audio::PcmSubformat Format_Raw = audio::PcmSubformat_Raw;
+const audio::PcmSubformat Format_S16_Be = audio::PcmSubformat_SInt16_Be;
+const audio::PcmSubformat Format_S16_Ne = audio::PcmSubformat_SInt16;
+const audio::PcmSubformat Format_S32_Ne = audio::PcmSubformat_SInt32;
 
 const rtp::PayloadType PayloadType_Ch1 = rtp::PayloadType_L16_Mono;
 const rtp::PayloadType PayloadType_Ch2 = rtp::PayloadType_L16_Stereo;
@@ -310,19 +310,19 @@ TEST_GROUP(receiver_source) {
     }
 
     void init_with_specs(int output_sample_rate, audio::ChannelMask output_channels,
-                         audio::PcmFormat output_format, int packet_sample_rate,
+                         audio::PcmSubformat output_format, int packet_sample_rate,
                          audio::ChannelMask packet_channels,
-                         audio::PcmFormat packet_format) {
+                         audio::PcmSubformat packet_format) {
+        output_sample_spec.set_format(audio::Format_Pcm);
+        output_sample_spec.set_pcm_subformat(output_format);
         output_sample_spec.set_sample_rate((size_t)output_sample_rate);
-        output_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        output_sample_spec.set_pcm_format(output_format);
         output_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         output_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         output_sample_spec.channel_set().set_mask(output_channels);
 
+        packet_sample_spec.set_format(audio::Format_Pcm);
+        packet_sample_spec.set_pcm_subformat(packet_format);
         packet_sample_spec.set_sample_rate((size_t)packet_sample_rate);
-        packet_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        packet_sample_spec.set_pcm_format(packet_format);
         packet_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         packet_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         packet_sample_spec.channel_set().set_mask(packet_channels);
@@ -2941,8 +2941,8 @@ TEST(receiver_source, big_read) {
 TEST(receiver_source, channel_mapping_stereo_to_mono) {
     enum { Rate = SampleRate, OutputChans = Chans_Mono, PacketChans = Chans_Stereo };
 
-    const audio::PcmFormat OutputFormat = Format_Raw;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat OutputFormat = Format_Raw;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, OutputChans, OutputFormat, Rate, PacketChans, PacketFormat);
 
@@ -2980,8 +2980,8 @@ TEST(receiver_source, channel_mapping_stereo_to_mono) {
 TEST(receiver_source, channel_mapping_mono_to_stereo) {
     enum { Rate = SampleRate, OutputChans = Chans_Stereo, PacketChans = Chans_Mono };
 
-    const audio::PcmFormat OutputFormat = Format_Raw;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat OutputFormat = Format_Raw;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, OutputChans, OutputFormat, Rate, PacketChans, PacketFormat);
 
@@ -3019,8 +3019,8 @@ TEST(receiver_source, channel_mapping_mono_to_stereo) {
 TEST(receiver_source, sample_rate_mapping) {
     enum { OutputRate = 48000, PacketRate = 44100, Chans = Chans_Stereo };
 
-    const audio::PcmFormat OutputFormat = Format_Raw;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat OutputFormat = Format_Raw;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(OutputRate, Chans, OutputFormat, PacketRate, Chans, PacketFormat);
 
@@ -3060,8 +3060,8 @@ TEST(receiver_source, sample_rate_mapping) {
 TEST(receiver_source, format_mapping_s16) {
     enum { Rate = SampleRate, Chans = Chans_Stereo };
 
-    const audio::PcmFormat OutputFormat = Format_S16_Ne;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat OutputFormat = Format_S16_Ne;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, Chans, OutputFormat, Rate, Chans, PacketFormat);
 
@@ -3098,8 +3098,8 @@ TEST(receiver_source, format_mapping_s16) {
 TEST(receiver_source, format_mapping_s32) {
     enum { Rate = SampleRate, Chans = Chans_Stereo };
 
-    const audio::PcmFormat OutputFormat = Format_S32_Ne;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat OutputFormat = Format_S32_Ne;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, Chans, OutputFormat, Rate, Chans, PacketFormat);
 
@@ -3319,8 +3319,8 @@ TEST(receiver_source, timestamp_mapping_remixing) {
         PacketChans = Chans_Mono
     };
 
-    const audio::PcmFormat OutputFormat = Format_S16_Ne;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat OutputFormat = Format_S16_Ne;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(OutputRate, OutputChans, OutputFormat, PacketRate, PacketChans,
                     PacketFormat);
diff --git a/src/tests/roc_pipeline/test_sender_sink.cpp b/src/tests/roc_pipeline/test_sender_sink.cpp
index dcb5e84be..ef53f6679 100644
--- a/src/tests/roc_pipeline/test_sender_sink.cpp
+++ b/src/tests/roc_pipeline/test_sender_sink.cpp
@@ -44,10 +44,10 @@ namespace {
 const audio::ChannelMask Chans_Mono = audio::ChanMask_Surround_Mono;
 const audio::ChannelMask Chans_Stereo = audio::ChanMask_Surround_Stereo;
 
-const audio::PcmFormat Format_Raw = audio::Sample_RawFormat;
-const audio::PcmFormat Format_S16_Be = audio::PcmFormat_SInt16_Be;
-const audio::PcmFormat Format_S16_Ne = audio::PcmFormat_SInt16;
-const audio::PcmFormat Format_S32_Ne = audio::PcmFormat_SInt32;
+const audio::PcmSubformat Format_Raw = audio::PcmSubformat_Raw;
+const audio::PcmSubformat Format_S16_Be = audio::PcmSubformat_SInt16_Be;
+const audio::PcmSubformat Format_S16_Ne = audio::PcmSubformat_SInt16;
+const audio::PcmSubformat Format_S32_Ne = audio::PcmSubformat_SInt32;
 
 const rtp::PayloadType PayloadType_Ch1 = rtp::PayloadType_L16_Mono;
 const rtp::PayloadType PayloadType_Ch2 = rtp::PayloadType_L16_Stereo;
@@ -171,19 +171,19 @@ TEST_GROUP(sender_sink) {
     }
 
     void init_with_specs(int input_sample_rate, audio::ChannelMask input_channels,
-                         audio::PcmFormat input_format, int packet_sample_rate,
+                         audio::PcmSubformat input_format, int packet_sample_rate,
                          audio::ChannelMask packet_channels,
-                         audio::PcmFormat packet_format) {
+                         audio::PcmSubformat packet_format) {
+        input_sample_spec.set_format(audio::Format_Pcm);
+        input_sample_spec.set_pcm_subformat(input_format);
         input_sample_spec.set_sample_rate((size_t)input_sample_rate);
-        input_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        input_sample_spec.set_pcm_format(input_format);
         input_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         input_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         input_sample_spec.channel_set().set_mask(input_channels);
 
+        packet_sample_spec.set_format(audio::Format_Pcm);
+        packet_sample_spec.set_pcm_subformat(packet_format);
         packet_sample_spec.set_sample_rate((size_t)packet_sample_rate);
-        packet_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        packet_sample_spec.set_pcm_format(packet_format);
         packet_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         packet_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         packet_sample_spec.channel_set().set_mask(packet_channels);
@@ -308,8 +308,8 @@ TEST(sender_sink, frame_size_large) {
 TEST(sender_sink, channel_mapping_stereo_to_mono) {
     enum { Rate = SampleRate, InputChans = Chans_Stereo, PacketChans = Chans_Mono };
 
-    const audio::PcmFormat InputFormat = Format_Raw;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat InputFormat = Format_Raw;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, InputChans, InputFormat, Rate, PacketChans, PacketFormat);
 
@@ -343,8 +343,8 @@ TEST(sender_sink, channel_mapping_stereo_to_mono) {
 TEST(sender_sink, channel_mapping_mono_to_stereo) {
     enum { Rate = SampleRate, InputChans = Chans_Mono, PacketChans = Chans_Stereo };
 
-    const audio::PcmFormat InputFormat = Format_Raw;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat InputFormat = Format_Raw;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, InputChans, InputFormat, Rate, PacketChans, PacketFormat);
 
@@ -378,8 +378,8 @@ TEST(sender_sink, channel_mapping_mono_to_stereo) {
 TEST(sender_sink, sample_rate_mapping) {
     enum { InputRate = 48000, PacketRate = 44100, Chans = Chans_Stereo };
 
-    const audio::PcmFormat InputFormat = Format_Raw;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat InputFormat = Format_Raw;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(InputRate, Chans, InputFormat, PacketRate, Chans, PacketFormat);
 
@@ -413,8 +413,8 @@ TEST(sender_sink, sample_rate_mapping) {
 TEST(sender_sink, format_mapping_s16) {
     enum { Rate = SampleRate, Chans = Chans_Stereo };
 
-    const audio::PcmFormat InputFormat = Format_S16_Ne;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat InputFormat = Format_S16_Ne;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, Chans, InputFormat, Rate, Chans, PacketFormat);
 
@@ -447,8 +447,8 @@ TEST(sender_sink, format_mapping_s16) {
 TEST(sender_sink, format_mapping_s32) {
     enum { Rate = SampleRate, Chans = Chans_Stereo };
 
-    const audio::PcmFormat InputFormat = Format_S32_Ne;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat InputFormat = Format_S32_Ne;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(Rate, Chans, InputFormat, Rate, Chans, PacketFormat);
 
@@ -519,8 +519,8 @@ TEST(sender_sink, timestamp_mapping_remixing) {
         PacketChans = Chans_Mono
     };
 
-    const audio::PcmFormat InputFormat = Format_S16_Ne;
-    const audio::PcmFormat PacketFormat = Format_S16_Be;
+    const audio::PcmSubformat InputFormat = Format_S16_Ne;
+    const audio::PcmSubformat PacketFormat = Format_S16_Be;
 
     init_with_specs(InputRate, InputChans, InputFormat, PacketRate, PacketChans,
                     PacketFormat);
diff --git a/src/tests/roc_pipeline/test_transcoder_sink.cpp b/src/tests/roc_pipeline/test_transcoder_sink.cpp
index cf7b8b5a8..889274114 100644
--- a/src/tests/roc_pipeline/test_transcoder_sink.cpp
+++ b/src/tests/roc_pipeline/test_transcoder_sink.cpp
@@ -63,16 +63,16 @@ TEST_GROUP(transcoder_sink) {
 
     void init(int input_sample_rate, audio::ChannelMask input_channels,
               int output_sample_rate, audio::ChannelMask output_channels) {
+        input_sample_spec.set_format(audio::Format_Pcm);
+        input_sample_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
         input_sample_spec.set_sample_rate((size_t)input_sample_rate);
-        input_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        input_sample_spec.set_pcm_format(audio::Sample_RawFormat);
         input_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         input_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         input_sample_spec.channel_set().set_mask(input_channels);
 
+        output_sample_spec.set_format(audio::Format_Pcm);
+        output_sample_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
         output_sample_spec.set_sample_rate((size_t)output_sample_rate);
-        output_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        output_sample_spec.set_pcm_format(audio::Sample_RawFormat);
         output_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         output_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         output_sample_spec.channel_set().set_mask(output_channels);
diff --git a/src/tests/roc_pipeline/test_transcoder_source.cpp b/src/tests/roc_pipeline/test_transcoder_source.cpp
index 6013503e7..3d90358fa 100644
--- a/src/tests/roc_pipeline/test_transcoder_source.cpp
+++ b/src/tests/roc_pipeline/test_transcoder_source.cpp
@@ -74,16 +74,16 @@ TEST_GROUP(transcoder_source) {
     }
 
     void init(audio::ChannelMask input_channels, audio::ChannelMask output_channels) {
+        input_sample_spec.set_format(audio::Format_Pcm);
+        input_sample_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
         input_sample_spec.set_sample_rate(SampleRate);
-        input_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        input_sample_spec.set_pcm_format(audio::Sample_RawFormat);
         input_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         input_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         input_sample_spec.channel_set().set_mask(input_channels);
 
+        output_sample_spec.set_format(audio::Format_Pcm);
+        output_sample_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
         output_sample_spec.set_sample_rate(SampleRate);
-        output_sample_spec.set_sample_format(audio::SampleFormat_Pcm);
-        output_sample_spec.set_pcm_format(audio::Sample_RawFormat);
         output_sample_spec.channel_set().set_layout(audio::ChanLayout_Surround);
         output_sample_spec.channel_set().set_order(audio::ChanOrder_Smpte);
         output_sample_spec.channel_set().set_mask(output_channels);
diff --git a/src/tests/roc_rtp/test_encoding.cpp b/src/tests/roc_rtp/test_encoding.cpp
index ab2674c04..924a222d2 100644
--- a/src/tests/roc_rtp/test_encoding.cpp
+++ b/src/tests/roc_rtp/test_encoding.cpp
@@ -17,13 +17,13 @@ TEST_GROUP(encoding) {};
 
 IGNORE_TEST(encoding, parse) {
     Encoding enc;
-    CHECK(parse_encoding("101:s18/48000/surround4.1", enc));
+    CHECK(parse_encoding("101:pcm@s18/48000/surround4.1", enc));
 
     CHECK_EQUAL(101, enc.payload_type);
 
-    CHECK(enc.sample_spec.is_valid());
-    CHECK_EQUAL(audio::SampleFormat_Pcm, enc.sample_spec.sample_format());
-    CHECK_EQUAL(audio::PcmFormat_SInt18, enc.sample_spec.pcm_format());
+    CHECK(enc.sample_spec.is_complete());
+    CHECK_EQUAL(audio::Format_Pcm, enc.sample_spec.format());
+    CHECK_EQUAL(audio::PcmSubformat_SInt18, enc.sample_spec.pcm_subformat());
     CHECK_EQUAL(48000, enc.sample_spec.sample_rate());
     CHECK_EQUAL(5, enc.sample_spec.num_channels());
 
@@ -35,20 +35,20 @@ IGNORE_TEST(encoding, parse) {
 TEST(encoding, parse_errors) {
     Encoding enc;
 
-    CHECK(!parse_encoding(":s16/44100/stereo", enc));
-    CHECK(!parse_encoding("101,s16/44100/stereo", enc));
+    CHECK(!parse_encoding(":pcm@s16/44100/stereo", enc));
+    CHECK(!parse_encoding("101,pcm@s16/44100/stereo", enc));
     CHECK(!parse_encoding("101:", enc));
-    CHECK(!parse_encoding("101:s16/44100/bad", enc));
+    CHECK(!parse_encoding("101:pcm@s16/44100/bad", enc));
     CHECK(!parse_encoding(":", enc));
     CHECK(!parse_encoding("", enc));
     CHECK(!parse_encoding("::", enc));
-    CHECK(!parse_encoding("101::s16/44100/stereo", enc));
-    CHECK(!parse_encoding("xxx:s16/44100/stereo", enc));
-    CHECK(!parse_encoding("-101:s16/44100/stereo", enc));
-    CHECK(!parse_encoding("+101:s16/44100/stereo", enc));
-    CHECK(!parse_encoding("101.2:s16/44100/stereo", enc));
+    CHECK(!parse_encoding("101::pcm@s16/44100/stereo", enc));
+    CHECK(!parse_encoding("xxx:pcm@s16/44100/stereo", enc));
+    CHECK(!parse_encoding("-101:pcm@s16/44100/stereo", enc));
+    CHECK(!parse_encoding("+101:pcm@s16/44100/stereo", enc));
+    CHECK(!parse_encoding("101.2:pcm@s16/44100/stereo", enc));
 
-    CHECK(parse_encoding("101:s16/44100/stereo", enc));
+    CHECK(parse_encoding("101:pcm@s16/44100/stereo", enc));
 }
 
 } // namespace rtp
diff --git a/src/tests/roc_rtp/test_encoding_map.cpp b/src/tests/roc_rtp/test_encoding_map.cpp
index efe72a23e..a9ba8efff 100644
--- a/src/tests/roc_rtp/test_encoding_map.cpp
+++ b/src/tests/roc_rtp/test_encoding_map.cpp
@@ -10,7 +10,7 @@
 
 #include "roc_audio/pcm_decoder.h"
 #include "roc_audio/pcm_encoder.h"
-#include "roc_audio/pcm_format.h"
+#include "roc_audio/pcm_subformat.h"
 #include "roc_core/heap_arena.h"
 #include "roc_rtp/encoding_map.h"
 
@@ -35,9 +35,9 @@ TEST(encoding_map, find_by_pt) {
 
         LONGS_EQUAL(PayloadType_L16_Mono, enc->payload_type);
 
-        CHECK(enc->sample_spec.is_valid());
+        CHECK(enc->sample_spec.is_complete());
         CHECK(enc->sample_spec
-              == audio::SampleSpec(44100, audio::PcmFormat_SInt16_Be,
+              == audio::SampleSpec(44100, audio::PcmSubformat_SInt16_Be,
                                    audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
                                    audio::ChanMask_Surround_Mono));
 
@@ -53,9 +53,9 @@ TEST(encoding_map, find_by_pt) {
 
         LONGS_EQUAL(PayloadType_L16_Stereo, enc->payload_type);
 
-        CHECK(enc->sample_spec.is_valid());
+        CHECK(enc->sample_spec.is_complete());
         CHECK(enc->sample_spec
-              == audio::SampleSpec(44100, audio::PcmFormat_SInt16_Be,
+              == audio::SampleSpec(44100, audio::PcmSubformat_SInt16_Be,
                                    audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
                                    audio::ChanMask_Surround_Stereo));
 
@@ -71,7 +71,7 @@ TEST(encoding_map, find_by_spec) {
 
     {
         const Encoding* enc = enc_map.find_by_spec(audio::SampleSpec(
-            48000, audio::PcmFormat_SInt16_Be, audio::ChanLayout_Surround,
+            48000, audio::PcmSubformat_SInt16_Be, audio::ChanLayout_Surround,
             audio::ChanOrder_Smpte, audio::ChanMask_Surround_Mono));
 
         CHECK(!enc);
@@ -79,7 +79,7 @@ TEST(encoding_map, find_by_spec) {
 
     {
         const Encoding* enc = enc_map.find_by_spec(audio::SampleSpec(
-            44100, audio::PcmFormat_SInt16_Be, audio::ChanLayout_Surround,
+            44100, audio::PcmSubformat_SInt16_Be, audio::ChanLayout_Surround,
             audio::ChanOrder_Smpte, audio::ChanMask_Surround_Mono));
 
         CHECK(enc);
@@ -89,7 +89,7 @@ TEST(encoding_map, find_by_spec) {
 
     {
         const Encoding* enc = enc_map.find_by_spec(audio::SampleSpec(
-            44100, audio::PcmFormat_SInt16_Be, audio::ChanLayout_Surround,
+            44100, audio::PcmSubformat_SInt16_Be, audio::ChanLayout_Surround,
             audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo));
 
         CHECK(enc);
@@ -105,9 +105,9 @@ TEST(encoding_map, add_encoding) {
         Encoding enc;
         enc.payload_type = (PayloadType)100;
         enc.packet_flags = packet::Packet::FlagAudio;
-        enc.sample_spec =
-            audio::SampleSpec(48000, audio::PcmFormat_SInt32, audio::ChanLayout_Surround,
-                              audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo);
+        enc.sample_spec = audio::SampleSpec(
+            48000, audio::PcmSubformat_SInt32, audio::ChanLayout_Surround,
+            audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo);
         enc.new_encoder = &audio::PcmEncoder::construct;
         enc.new_decoder = &audio::PcmDecoder::construct;
 
@@ -121,7 +121,7 @@ TEST(encoding_map, add_encoding) {
         LONGS_EQUAL(100, enc->payload_type);
 
         CHECK(enc->sample_spec
-              == audio::SampleSpec(48000, audio::PcmFormat_SInt32,
+              == audio::SampleSpec(48000, audio::PcmSubformat_SInt32,
                                    audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
                                    audio::ChanMask_Surround_Stereo));
 
@@ -132,15 +132,15 @@ TEST(encoding_map, add_encoding) {
     }
 
     {
-        const Encoding* enc = enc_map.find_by_spec(
-            audio::SampleSpec(48000, audio::PcmFormat_SInt32, audio::ChanLayout_Surround,
-                              audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo));
+        const Encoding* enc = enc_map.find_by_spec(audio::SampleSpec(
+            48000, audio::PcmSubformat_SInt32, audio::ChanLayout_Surround,
+            audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo));
         CHECK(enc);
 
         LONGS_EQUAL(100, enc->payload_type);
 
         CHECK(enc->sample_spec
-              == audio::SampleSpec(48000, audio::PcmFormat_SInt32,
+              == audio::SampleSpec(48000, audio::PcmSubformat_SInt32,
                                    audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
                                    audio::ChanMask_Surround_Stereo));
 
diff --git a/src/tests/roc_rtp/test_filter.cpp b/src/tests/roc_rtp/test_filter.cpp
index 822bbd32c..6a11edf19 100644
--- a/src/tests/roc_rtp/test_filter.cpp
+++ b/src/tests/roc_rtp/test_filter.cpp
@@ -37,7 +37,7 @@ enum {
 };
 
 const audio::SampleSpec payload_spec(SampleRate,
-                                     audio::PcmFormat_SInt16_Be,
+                                     audio::PcmSubformat_SInt16_Be,
                                      audio::ChanLayout_Surround,
                                      audio::ChanOrder_Smpte,
                                      ChMask);
diff --git a/src/tests/roc_rtp/test_link_meter.cpp b/src/tests/roc_rtp/test_link_meter.cpp
index b00f1e010..06e951622 100644
--- a/src/tests/roc_rtp/test_link_meter.cpp
+++ b/src/tests/roc_rtp/test_link_meter.cpp
@@ -41,7 +41,7 @@ packet::PacketFactory packet_factory(arena, PacketSz);
 EncodingMap encoding_map(arena);
 
 audio::SampleSpec sample_spec(SampleRate,
-                              audio::Sample_RawFormat,
+                              audio::PcmSubformat_Raw,
                               audio::ChanLayout_Surround,
                               audio::ChanOrder_Smpte,
                               ChMask);
diff --git a/src/tests/roc_rtp/test_timestamp_extractor.cpp b/src/tests/roc_rtp/test_timestamp_extractor.cpp
index f87e3947c..2117245b6 100644
--- a/src/tests/roc_rtp/test_timestamp_extractor.cpp
+++ b/src/tests/roc_rtp/test_timestamp_extractor.cpp
@@ -46,7 +46,7 @@ TEST_GROUP(timestamp_extractor) {};
 TEST(timestamp_extractor, single_write) {
     // 1 second = 1000 samples
     const audio::SampleSpec sample_spec =
-        audio::SampleSpec(1000, audio::Sample_RawFormat, audio::ChanLayout_Surround,
+        audio::SampleSpec(1000, audio::PcmSubformat_Raw, audio::ChanLayout_Surround,
                           audio::ChanOrder_Smpte, 0x1);
 
     const core::nanoseconds_t cts = 1691499037871419405;
@@ -84,7 +84,7 @@ TEST(timestamp_extractor, single_write) {
 TEST(timestamp_extractor, forward_error) {
     // 1 second = 1000 samples
     const audio::SampleSpec sample_spec =
-        audio::SampleSpec(1000, audio::Sample_RawFormat, audio::ChanLayout_Surround,
+        audio::SampleSpec(1000, audio::PcmSubformat_Raw, audio::ChanLayout_Surround,
                           audio::ChanOrder_Smpte, 0x1);
 
     const status::StatusCode status_list[] = {
diff --git a/src/tests/roc_rtp/test_timestamp_injector.cpp b/src/tests/roc_rtp/test_timestamp_injector.cpp
index 0525676ca..ed20c6c27 100644
--- a/src/tests/roc_rtp/test_timestamp_injector.cpp
+++ b/src/tests/roc_rtp/test_timestamp_injector.cpp
@@ -69,7 +69,7 @@ TEST(timestamp_injector, negative_and_positive_dn) {
 
     const float sample_rate = 48000.;
     const audio::SampleSpec sample_spec =
-        audio::SampleSpec((size_t)sample_rate, audio::Sample_RawFormat,
+        audio::SampleSpec((size_t)sample_rate, audio::PcmSubformat_Raw,
                           audio::ChanLayout_Surround, audio::ChanOrder_Smpte, ChMask);
 
     packet::stream_timestamp_t rtp_ts = 2222;
@@ -120,7 +120,7 @@ TEST(timestamp_injector, fetch_peek) {
     };
 
     const audio::SampleSpec sample_spec =
-        audio::SampleSpec(SampleRate, audio::Sample_RawFormat, audio::ChanLayout_Surround,
+        audio::SampleSpec(SampleRate, audio::PcmSubformat_Raw, audio::ChanLayout_Surround,
                           audio::ChanOrder_Smpte, ChMask);
 
     packet::FifoQueue queue;
@@ -167,7 +167,7 @@ TEST(timestamp_injector, forward_error) {
     };
 
     const audio::SampleSpec sample_spec =
-        audio::SampleSpec(SampleRate, audio::Sample_RawFormat, audio::ChanLayout_Surround,
+        audio::SampleSpec(SampleRate, audio::PcmSubformat_Raw, audio::ChanLayout_Surround,
                           audio::ChanOrder_Smpte, ChMask);
 
     const status::StatusCode status_list[] = {
diff --git a/src/tests/roc_sndio/test_backend_sink.cpp b/src/tests/roc_sndio/test_backend_sink.cpp
deleted file mode 100644
index abc59d1e7..000000000
--- a/src/tests/roc_sndio/test_backend_sink.cpp
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (c) 2015 Roc Streaming authors
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-#include <CppUTest/TestHarness.h>
-
-#include "test_helpers/utils.h"
-
-#include "roc_core/heap_arena.h"
-#include "roc_core/scoped_ptr.h"
-#include "roc_dbgio/temp_file.h"
-#include "roc_sndio/backend_map.h"
-#include "roc_sndio/io_pump.h"
-
-namespace roc {
-namespace sndio {
-
-namespace {
-
-enum { FrameSize = 500, SampleRate = 48000 };
-
-const audio::SampleSpec sample_spec(SampleRate,
-                                    audio::Sample_RawFormat,
-                                    audio::ChanLayout_Surround,
-                                    audio::ChanOrder_Smpte,
-                                    audio::ChanMask_Surround_Stereo);
-
-const core::nanoseconds_t frame_duration = FrameSize * core::Second
-    / core::nanoseconds_t(sample_spec.sample_rate() * sample_spec.num_channels());
-
-core::HeapArena arena;
-audio::FrameFactory frame_factory(arena, FrameSize * sizeof(audio::sample_t));
-
-} // namespace
-
-TEST_GROUP(backend_sink) {
-    IoConfig sink_config;
-
-    void setup() {
-        sink_config.sample_spec = sample_spec;
-        sink_config.frame_length = frame_duration;
-    }
-};
-
-TEST(backend_sink, open) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-
-        core::ScopedPtr<ISink> backend_sink;
-        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena,
-                               DriverType_File, NULL, file.path(), sink_config,
-                               backend_sink);
-
-        test::expect_specs_equal(backend.name(), sink_config.sample_spec,
-                                 backend_sink->sample_spec());
-
-        CHECK(!backend_sink->has_state());
-        CHECK(!backend_sink->has_latency());
-        CHECK(!backend_sink->has_clock());
-        LONGS_EQUAL(status::StatusOK, backend_sink->close());
-    }
-}
-
-// Open fails because file doesn't exist.
-TEST(backend_sink, open_bad_file) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        core::ScopedPtr<ISink> backend_sink;
-        test::expect_open_sink(status::StatusErrFile, backend, frame_factory, arena,
-                               DriverType_File, NULL, "/bad/file.wav", sink_config,
-                               backend_sink);
-    }
-}
-
-// Open fails because of invalid sndio::Config.
-TEST(backend_sink, open_bad_config) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-
-        IoConfig bad_config = sink_config;
-        bad_config.sample_spec.set_pcm_format(audio::PcmFormat_SInt18_3_Be);
-
-        core::ScopedPtr<ISink> backend_sink;
-        test::expect_open_sink(status::StatusBadConfig, backend, frame_factory, arena,
-                               DriverType_File, NULL, file.path(), bad_config,
-                               backend_sink);
-    }
-}
-
-// If config is empty, open uses default values.
-TEST(backend_sink, open_default_config) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-
-        IoConfig default_config = sink_config;
-        default_config.sample_spec.clear();
-
-        core::ScopedPtr<ISink> backend_sink;
-        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena,
-                               DriverType_File, NULL, file.path(), default_config,
-                               backend_sink);
-
-        CHECK(backend_sink->sample_spec().is_valid());
-        LONGS_EQUAL(status::StatusOK, backend_sink->close());
-    }
-}
-
-} // namespace sndio
-} // namespace roc
diff --git a/src/tests/roc_sndio/test_backend_source.cpp b/src/tests/roc_sndio/test_backend_source.cpp
deleted file mode 100644
index c133ccd6c..000000000
--- a/src/tests/roc_sndio/test_backend_source.cpp
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (c) 2023 Roc Streaming authors
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-#include <CppUTest/TestHarness.h>
-
-#include "test_helpers/mock_source.h"
-#include "test_helpers/utils.h"
-
-#include "roc_core/heap_arena.h"
-#include "roc_core/scoped_ptr.h"
-#include "roc_core/slab_pool.h"
-#include "roc_dbgio/temp_file.h"
-#include "roc_sndio/backend_map.h"
-#include "roc_sndio/io_pump.h"
-
-namespace roc {
-namespace sndio {
-
-namespace {
-
-enum { MaxBufSize = 8192, FrameSize = 500, SampleRate = 48000 };
-
-const audio::SampleSpec sample_spec(SampleRate,
-                                    audio::Sample_RawFormat,
-                                    audio::ChanLayout_Surround,
-                                    audio::ChanOrder_Smpte,
-                                    audio::ChanMask_Surround_Stereo);
-
-const core::nanoseconds_t frame_duration = FrameSize * core::Second
-    / core::nanoseconds_t(sample_spec.sample_rate() * sample_spec.num_channels());
-
-core::HeapArena arena;
-
-core::SlabPool<audio::Frame> frame_pool("frame_pool", arena);
-core::SlabPool<core::Buffer>
-    frame_buffer_pool("frame_buffer_pool",
-                      arena,
-                      sizeof(core::Buffer) + MaxBufSize * sizeof(audio::sample_t));
-
-audio::FrameFactory frame_factory(frame_pool, frame_buffer_pool);
-
-void write_wav(IBackend& backend,
-               const IoConfig& config,
-               const char* path,
-               size_t num_samples) {
-    test::MockSource mock_source(frame_factory, config.sample_spec, arena);
-    mock_source.add(num_samples * sample_spec.num_channels());
-
-    IDevice* backend_device = NULL;
-    LONGS_EQUAL(status::StatusOK,
-                backend.open_device(DeviceType_Sink, DriverType_File, NULL, path, config,
-                                    frame_factory, arena, &backend_device));
-    CHECK(backend_device != NULL);
-    core::ScopedPtr<ISink> backend_sink(backend_device->to_sink());
-    CHECK(backend_sink != NULL);
-
-    IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *backend_sink, config,
-                IoPump::ModeOneshot);
-    LONGS_EQUAL(status::StatusOK, pump.init_status());
-    LONGS_EQUAL(status::StatusOK, pump.run());
-}
-
-void expect_read(status::StatusCode expected_code,
-                 ISource& source,
-                 audio::Frame& frame,
-                 packet::stream_timestamp_t requested_samples) {
-    const status::StatusCode code =
-        source.read(frame, requested_samples, audio::ModeHard);
-
-    LONGS_EQUAL(expected_code, code);
-}
-
-} // namespace
-
-TEST_GROUP(backend_source) {
-    IoConfig sink_config;
-    IoConfig source_config;
-
-    void setup() {
-        sink_config.sample_spec = sample_spec;
-        sink_config.frame_length = frame_duration;
-
-        source_config.sample_spec = audio::SampleSpec();
-        source_config.frame_length = frame_duration;
-    }
-};
-
-TEST(backend_source, open) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-        write_wav(backend, sink_config, file.path(), MaxBufSize * 10);
-
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusOK, backend, frame_factory, arena,
-                                 DriverType_File, NULL, file.path(), source_config,
-                                 backend_source);
-
-        test::expect_specs_equal(backend.name(), sink_config.sample_spec,
-                                 backend_source->sample_spec());
-
-        CHECK(!backend_source->has_state());
-        CHECK(!backend_source->has_latency());
-        CHECK(!backend_source->has_clock());
-        LONGS_EQUAL(status::StatusOK, backend_source->close());
-    }
-}
-
-// Open fails because file doesn't exist.
-TEST(backend_source, open_bad_file) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusErrFile, backend, frame_factory, arena,
-                                 DriverType_File, NULL, "/bad/file.wav", source_config,
-                                 backend_source);
-    }
-}
-
-// Open fails because of invalid sndio::Config.
-TEST(backend_source, open_bad_config) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-        write_wav(backend, sink_config, file.path(), MaxBufSize * 10);
-
-        IoConfig bad_config = source_config;
-        bad_config.sample_spec.set_sample_rate(SampleRate);
-
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusBadConfig, backend, frame_factory, arena,
-                                 DriverType_File, NULL, file.path(), bad_config,
-                                 backend_source);
-    }
-}
-
-// Rewind and read same frame again.
-TEST(backend_source, rewind) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-        write_wav(backend, sink_config, file.path(), MaxBufSize * 10);
-
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusOK, backend, frame_factory, arena,
-                                 DriverType_File, "wav", file.path(), source_config,
-                                 backend_source);
-
-        audio::FramePtr frame1 = frame_factory.allocate_frame_no_buffer();
-        CHECK(frame1);
-        expect_read(status::StatusOK, *backend_source, *frame1, FrameSize);
-
-        // rewind
-        LONGS_EQUAL(status::StatusOK, backend_source->rewind());
-
-        audio::FramePtr frame2 = frame_factory.allocate_frame_no_buffer();
-        CHECK(frame2);
-        expect_read(status::StatusOK, *backend_source, *frame2, FrameSize);
-
-        LONGS_EQUAL(FrameSize * sample_spec.num_channels(), frame1->num_raw_samples());
-        LONGS_EQUAL(FrameSize * sample_spec.num_channels(), frame2->num_raw_samples());
-
-        if (memcmp(frame1->raw_samples(), frame2->raw_samples(),
-                   frame1->num_raw_samples() * sizeof(audio::sample_t))
-            != 0) {
-            FAIL("frames should be equal");
-        }
-        LONGS_EQUAL(status::StatusOK, backend_source->close());
-    }
-}
-
-// Read until EOF, rewind, repeat.
-TEST(backend_source, rewind_after_eof) {
-    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
-         n_backend++) {
-        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
-        if (!test::backend_supports_format(backend, arena, "wav")) {
-            continue;
-        }
-
-        dbgio::TempFile file("test.wav");
-        write_wav(backend, sink_config, file.path(), FrameSize * 2);
-
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusOK, backend, frame_factory, arena,
-                                 DriverType_File, "wav", file.path(), source_config,
-                                 backend_source);
-
-        audio::FramePtr frame = frame_factory.allocate_frame_no_buffer();
-        CHECK(frame);
-
-        for (int i = 0; i < 10; i++) {
-            expect_read(status::StatusOK, *backend_source, *frame, FrameSize);
-            expect_read(status::StatusOK, *backend_source, *frame, FrameSize);
-            expect_read(status::StatusFinish, *backend_source, *frame, FrameSize);
-
-            // rewind
-            LONGS_EQUAL(status::StatusOK, backend_source->rewind());
-        }
-        LONGS_EQUAL(status::StatusOK, backend_source->close());
-    }
-}
-
-} // namespace sndio
-
-} // namespace roc
diff --git a/src/tests/roc_sndio/test_helpers/mock_sink.h b/src/tests/roc_sndio/test_helpers/mock_sink.h
index 5ad9b4f67..fbfd042a0 100644
--- a/src/tests/roc_sndio/test_helpers/mock_sink.h
+++ b/src/tests/roc_sndio/test_helpers/mock_sink.h
@@ -41,6 +41,10 @@ class MockSink : public ISink {
         return audio::SampleSpec();
     }
 
+    core::nanoseconds_t frame_length() const {
+        return 0;
+    }
+
     virtual bool has_state() const {
         return true;
     }
diff --git a/src/tests/roc_sndio/test_helpers/mock_source.h b/src/tests/roc_sndio/test_helpers/mock_source.h
index 2c127753f..465cbec2e 100644
--- a/src/tests/roc_sndio/test_helpers/mock_source.h
+++ b/src/tests/roc_sndio/test_helpers/mock_source.h
@@ -46,7 +46,11 @@ class MockSource : public ISource {
     }
 
     virtual audio::SampleSpec sample_spec() const {
-        return audio::SampleSpec();
+        return sample_spec_;
+    }
+
+    core::nanoseconds_t frame_length() const {
+        return 0;
     }
 
     virtual bool has_state() const {
@@ -97,24 +101,24 @@ class MockSource : public ISource {
             frame, sample_spec_.stream_timestamp_2_bytes(duration)));
 
         frame.set_raw(true);
-        frame.set_duration(duration);
 
-        size_t ns = frame.num_raw_samples();
-        if (ns > size_ - pos_) {
-            ns = size_ - pos_;
+        size_t n_samples = frame.num_raw_samples();
+        if (n_samples > size_ - pos_) {
+            n_samples = size_ - pos_;
         }
 
-        if (ns > 0) {
-            memcpy(frame.raw_samples(), samples_ + pos_, ns * sizeof(audio::sample_t));
-            pos_ += ns;
+        if (n_samples == 0) {
+            return status::StatusFinish;
         }
 
-        if (ns < frame.num_raw_samples()) {
-            memset(frame.raw_samples() + ns, 0,
-                   (frame.num_raw_samples() - ns) * sizeof(audio::sample_t));
-        }
+        memcpy(frame.raw_samples(), samples_ + pos_, n_samples * sizeof(audio::sample_t));
+        pos_ += n_samples;
 
-        return status::StatusOK;
+        frame.set_num_raw_samples(n_samples);
+        frame.set_duration((packet::stream_timestamp_t)n_samples
+                           / sample_spec_.num_channels());
+
+        return frame.duration() == duration ? status::StatusOK : status::StatusPart;
     }
 
     virtual status::StatusCode close() {
diff --git a/src/tests/roc_sndio/test_helpers/utils.h b/src/tests/roc_sndio/test_helpers/utils.h
index bd5f71a63..08617ca13 100644
--- a/src/tests/roc_sndio/test_helpers/utils.h
+++ b/src/tests/roc_sndio/test_helpers/utils.h
@@ -22,10 +22,10 @@ namespace test {
 namespace {
 
 bool backend_supports_format(IBackend& backend, core::IArena& arena, const char* format) {
-    core::Array<DriverInfo, MaxDrivers> driver_list(arena);
-    backend.discover_drivers(driver_list);
-    for (size_t n = 0; n < driver_list.size(); n++) {
-        if (strcmp(driver_list[n].name, format) == 0) {
+    core::Array<FormatInfo, MaxFormats> format_list(arena);
+    CHECK(backend.discover_formats(format_list));
+    for (size_t n = 0; n < format_list.size(); n++) {
+        if (strcmp(format_list[n].format_name, format) == 0) {
             return true;
         }
     }
@@ -36,15 +36,13 @@ void expect_open_sink(status::StatusCode expected_code,
                       IBackend& backend,
                       audio::FrameFactory& frame_factory,
                       core::IArena& arena,
-                      DriverType driver_type,
                       const char* driver,
                       const char* path,
                       const IoConfig& config,
                       core::ScopedPtr<ISink>& result) {
     IDevice* device = NULL;
-    const status::StatusCode code =
-        backend.open_device(DeviceType_Sink, driver_type, driver, path, config,
-                            frame_factory, arena, &device);
+    const status::StatusCode code = backend.open_device(
+        DeviceType_Sink, driver, path, config, frame_factory, arena, &device);
 
     if (code != expected_code) {
         char buf[1024] = {};
@@ -70,15 +68,13 @@ void expect_open_source(status::StatusCode expected_code,
                         IBackend& backend,
                         audio::FrameFactory& frame_factory,
                         core::IArena& arena,
-                        DriverType driver_type,
                         const char* driver,
                         const char* path,
                         const IoConfig& config,
                         core::ScopedPtr<ISource>& result) {
     IDevice* device = NULL;
-    const status::StatusCode code =
-        backend.open_device(DeviceType_Source, driver_type, driver, path, config,
-                            frame_factory, arena, &device);
+    const status::StatusCode code = backend.open_device(
+        DeviceType_Source, driver, path, config, frame_factory, arena, &device);
 
     if (code != expected_code) {
         char buf[1024] = {};
diff --git a/src/tests/roc_sndio/test_io_pump.cpp b/src/tests/roc_sndio/test_io_pump.cpp
index c539a1427..cb04bb640 100644
--- a/src/tests/roc_sndio/test_io_pump.cpp
+++ b/src/tests/roc_sndio/test_io_pump.cpp
@@ -28,7 +28,7 @@ namespace {
 enum { FrameSize = 512, SampleRate = 48000 };
 
 const audio::SampleSpec sample_spec(SampleRate,
-                                    audio::Sample_RawFormat,
+                                    audio::PcmSubformat_Raw,
                                     audio::ChanLayout_Surround,
                                     audio::ChanOrder_Smpte,
                                     audio::ChanMask_Surround_Stereo);
@@ -61,7 +61,7 @@ TEST_GROUP(io_pump) {
     }
 };
 
-TEST(io_pump, write_read) {
+IGNORE_TEST(io_pump, write_read) {
     enum { NumSamples = FrameSize * 10 };
 
     for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
@@ -77,14 +77,13 @@ TEST(io_pump, write_read) {
         mock_source.add(NumSamples);
 
         {
-            // open sink
-            core::ScopedPtr<ISink> backend_sink;
+            // open file sink
+            core::ScopedPtr<ISink> file_sink;
             test::expect_open_sink(status::StatusOK, backend, frame_factory, arena,
-                                   DriverType_File, "wav", file.path(), sink_config,
-                                   backend_sink);
+                                   "file", file.path(), sink_config, file_sink);
 
-            // copy from mock source to sink
-            IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *backend_sink,
+            // copy from mock source to file sink
+            IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *file_sink,
                         sink_config, IoPump::ModeOneshot);
             LONGS_EQUAL(status::StatusOK, pump.init_status());
             LONGS_EQUAL(status::StatusOK, pump.run());
@@ -92,15 +91,14 @@ TEST(io_pump, write_read) {
             CHECK(mock_source.num_returned() >= NumSamples - FrameSize);
         }
 
-        // open source
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusOK, backend, frame_factory, arena,
-                                 DriverType_File, "wav", file.path(), source_config,
-                                 backend_source);
+        // open file source
+        core::ScopedPtr<ISource> file_source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "file",
+                                 file.path(), source_config, file_source);
 
-        // copy from source to mock sink
+        // copy from file source to mock sink
         test::MockSink mock_sink(arena);
-        IoPump pump(frame_pool, frame_buffer_pool, *backend_source, NULL, mock_sink,
+        IoPump pump(frame_pool, frame_buffer_pool, *file_source, NULL, mock_sink,
                     sink_config, IoPump::ModePermanent);
         LONGS_EQUAL(status::StatusOK, pump.init_status());
         LONGS_EQUAL(status::StatusOK, pump.run());
@@ -110,7 +108,7 @@ TEST(io_pump, write_read) {
     }
 }
 
-TEST(io_pump, write_overwrite_read) {
+IGNORE_TEST(io_pump, write_overwrite_read) {
     enum { NumSamples = FrameSize * 10 };
 
     for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
@@ -126,14 +124,13 @@ TEST(io_pump, write_overwrite_read) {
         mock_source.add(NumSamples);
 
         {
-            // open sink
-            core::ScopedPtr<ISink> backend_sink;
-            test::expect_open_sink(status::StatusOK, backend, frame_factory, arena,
-                                   DriverType_File, "wav", file.path(), sink_config,
-                                   backend_sink);
+            // open file sink
+            core::ScopedPtr<ISink> file_sink;
+            test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "wav",
+                                   file.path(), sink_config, file_sink);
 
-            // copy from mock source to sink
-            IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *backend_sink,
+            // copy from mock source to file sink
+            IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *file_sink,
                         sink_config, IoPump::ModeOneshot);
             LONGS_EQUAL(status::StatusOK, pump.init_status());
             LONGS_EQUAL(status::StatusOK, pump.run());
@@ -146,14 +143,13 @@ TEST(io_pump, write_overwrite_read) {
         CHECK(num_returned1 >= NumSamples - FrameSize);
 
         {
-            // open sink
-            core::ScopedPtr<ISink> backend_sink;
-            test::expect_open_sink(status::StatusOK, backend, frame_factory, arena,
-                                   DriverType_File, "wav", file.path(), sink_config,
-                                   backend_sink);
+            // open file sink
+            core::ScopedPtr<ISink> file_sink;
+            test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "wav",
+                                   file.path(), sink_config, file_sink);
 
-            // copy next samples from mock source to sink, overwriting file
-            IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *backend_sink,
+            // copy next samples from mock source to file sink, overwriting file
+            IoPump pump(frame_pool, frame_buffer_pool, mock_source, NULL, *file_sink,
                         sink_config, IoPump::ModeOneshot);
             LONGS_EQUAL(status::StatusOK, pump.init_status());
             LONGS_EQUAL(status::StatusOK, pump.run());
@@ -162,15 +158,14 @@ TEST(io_pump, write_overwrite_read) {
         size_t num_returned2 = mock_source.num_returned() - num_returned1;
         CHECK(num_returned1 >= NumSamples - FrameSize);
 
-        // open source
-        core::ScopedPtr<ISource> backend_source;
-        test::expect_open_source(status::StatusOK, backend, frame_factory, arena,
-                                 DriverType_File, "wav", file.path(), source_config,
-                                 backend_source);
+        // open file source
+        core::ScopedPtr<ISource> file_source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "wav",
+                                 file.path(), source_config, file_source);
 
-        // copy from source to mock sink
+        // copy from file source to mock sink
         test::MockSink mock_sink(arena);
-        IoPump pump(frame_pool, frame_buffer_pool, *backend_source, NULL, mock_sink,
+        IoPump pump(frame_pool, frame_buffer_pool, *file_source, NULL, mock_sink,
                     sink_config, IoPump::ModePermanent);
         LONGS_EQUAL(status::StatusOK, pump.init_status());
         LONGS_EQUAL(status::StatusOK, pump.run());
@@ -179,5 +174,6 @@ TEST(io_pump, write_overwrite_read) {
         mock_sink.check(num_returned1, num_returned2);
     }
 }
+
 } // namespace sndio
 } // namespace roc
diff --git a/src/tests/roc_sndio/test_sinks.cpp b/src/tests/roc_sndio/test_sinks.cpp
new file mode 100644
index 000000000..c2034e972
--- /dev/null
+++ b/src/tests/roc_sndio/test_sinks.cpp
@@ -0,0 +1,429 @@
+/*
+ * Copyright (c) 2015 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <CppUTest/TestHarness.h>
+
+#include "test_helpers/mock_sink.h"
+#include "test_helpers/utils.h"
+
+#include "roc_core/heap_arena.h"
+#include "roc_core/macro_helpers.h"
+#include "roc_core/scoped_ptr.h"
+#include "roc_dbgio/temp_file.h"
+#include "roc_sndio/backend_map.h"
+#include "roc_sndio/isource.h"
+
+namespace roc {
+namespace sndio {
+
+namespace {
+
+enum { FrameSize = 500 };
+
+core::HeapArena arena;
+audio::FrameFactory frame_factory(arena, FrameSize * sizeof(audio::sample_t));
+
+void read_wav(IBackend& backend, const audio::SampleSpec& frame_spec, const char* path) {
+    const core::nanoseconds_t frame_len = FrameSize * core::Second
+        / core::nanoseconds_t(frame_spec.sample_rate() * frame_spec.num_channels());
+
+    test::MockSink mock_sink(arena);
+
+    IoConfig source_config;
+    source_config.sample_spec = audio::SampleSpec();
+    source_config.frame_length = frame_len;
+
+    IDevice* source_device = NULL;
+    LONGS_EQUAL(status::StatusOK,
+                backend.open_device(DeviceType_Source, "file", path, source_config,
+                                    frame_factory, arena, &source_device));
+    CHECK(source_device != NULL);
+
+    core::ScopedPtr<ISource> source(source_device->to_source());
+    CHECK(source != NULL);
+
+    for (;;) {
+        audio::FramePtr frame =
+            frame_factory.allocate_frame(frame_spec.ns_2_bytes(frame_len));
+
+        const status::StatusCode code = source->read(
+            *frame, frame_spec.ns_2_stream_timestamp(frame_len), audio::ModeHard);
+
+        CHECK(code == status::StatusOK || code == status::StatusPart
+              || code == status::StatusFinish);
+
+        if (code == status::StatusFinish) {
+            break;
+        }
+
+        LONGS_EQUAL(status::StatusOK, mock_sink.write(*frame));
+    }
+}
+
+audio::ChannelSet make_channel_set(audio::ChannelMask chans) {
+    audio::ChannelSet ch_set;
+    ch_set.set_layout(audio::ChanLayout_Surround);
+    ch_set.set_order(audio::ChanOrder_Smpte);
+    ch_set.set_mask(chans);
+
+    return ch_set;
+}
+
+IoConfig make_config(const audio::SampleSpec& file_spec,
+                     const audio::SampleSpec& frame_spec) {
+    IoConfig config;
+    config.sample_spec = file_spec;
+    config.frame_length = FrameSize * core::Second
+        / core::nanoseconds_t(frame_spec.sample_rate() * frame_spec.num_channels());
+
+    return config;
+}
+
+} // namespace
+
+TEST_GROUP(sinks) {};
+
+// Don't specify output spec.
+TEST(sinks, empty_spec) {
+    audio::SampleSpec file_spec;
+    file_spec.clear();
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "file",
+                               file.path(), make_config(file_spec, frame_spec), sink);
+
+        audio::SampleSpec actual_spec = sink->sample_spec();
+        CHECK(actual_spec.pcm_subformat() != audio::PcmSubformat_Invalid);
+        actual_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!sink->has_state());
+        CHECK(!sink->has_latency());
+        CHECK(!sink->has_clock());
+        LONGS_EQUAL(status::StatusOK, sink->close());
+    }
+}
+
+// Specify complete spec.
+TEST(sinks, complete_spec) {
+    audio::SampleSpec file_spec;
+    file_spec.set_format(audio::Format_Wav);
+    file_spec.set_pcm_subformat(audio::PcmSubformat_SInt16);
+    file_spec.set_sample_rate(48000);
+    file_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Mono));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(48000);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Mono));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "file",
+                               file.path(), make_config(file_spec, frame_spec), sink);
+
+        audio::SampleSpec actual_spec = sink->sample_spec();
+        CHECK(actual_spec.pcm_subformat() != audio::PcmSubformat_Invalid);
+        if (actual_spec.pcm_subformat() == audio::PcmSubformat_SInt16_Le) {
+            actual_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+        }
+
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!sink->has_state());
+        CHECK(!sink->has_latency());
+        CHECK(!sink->has_clock());
+        LONGS_EQUAL(status::StatusOK, sink->close());
+    }
+}
+
+// Specify only format.
+TEST(sinks, explicit_format) {
+    audio::SampleSpec file_spec;
+    file_spec.set_format(audio::Format_Wav);
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "file",
+                               file.path(), make_config(file_spec, frame_spec), sink);
+
+        audio::SampleSpec actual_spec = sink->sample_spec();
+        CHECK(actual_spec.pcm_subformat() != audio::PcmSubformat_Invalid);
+        actual_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!sink->has_state());
+        CHECK(!sink->has_latency());
+        CHECK(!sink->has_clock());
+        LONGS_EQUAL(status::StatusOK, sink->close());
+    }
+}
+
+// Specify only format and sub-format.
+TEST(sinks, explicit_format_and_subformat) {
+    audio::SampleSpec file_spec;
+    file_spec.set_format(audio::Format_Wav);
+    file_spec.set_pcm_subformat(audio::PcmSubformat_SInt16);
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "file",
+                               file.path(), make_config(file_spec, frame_spec), sink);
+
+        audio::SampleSpec actual_spec = sink->sample_spec();
+        CHECK(actual_spec.pcm_subformat() != audio::PcmSubformat_Invalid);
+        if (actual_spec.pcm_subformat() == audio::PcmSubformat_SInt16_Le) {
+            actual_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+        }
+
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!sink->has_state());
+        CHECK(!sink->has_latency());
+        CHECK(!sink->has_clock());
+        LONGS_EQUAL(status::StatusOK, sink->close());
+    }
+}
+
+// Specify only sample rate.
+TEST(sinks, explicit_rate) {
+    audio::SampleSpec file_spec;
+    file_spec.set_sample_rate(48000);
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(48000);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "file",
+                               file.path(), make_config(file_spec, frame_spec), sink);
+
+        audio::SampleSpec actual_spec = sink->sample_spec();
+        CHECK(actual_spec.pcm_subformat() != audio::PcmSubformat_Invalid);
+        actual_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!sink->has_state());
+        CHECK(!sink->has_latency());
+        CHECK(!sink->has_clock());
+        LONGS_EQUAL(status::StatusOK, sink->close());
+    }
+}
+
+// Specify only channel set.
+TEST(sinks, explicit_channels) {
+    audio::SampleSpec file_spec;
+    file_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Mono));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Mono));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusOK, backend, frame_factory, arena, "file",
+                               file.path(), make_config(file_spec, frame_spec), sink);
+
+        audio::SampleSpec actual_spec = sink->sample_spec();
+        CHECK(actual_spec.pcm_subformat() != audio::PcmSubformat_Invalid);
+        actual_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!sink->has_state());
+        CHECK(!sink->has_latency());
+        CHECK(!sink->has_clock());
+        LONGS_EQUAL(status::StatusOK, sink->close());
+    }
+}
+
+// Directory doesn't exist.
+TEST(sinks, bad_file_path) {
+    audio::SampleSpec file_spec;
+    file_spec.clear();
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusErrFile, backend, frame_factory, arena,
+                               "file", "/bad/file.wav",
+                               make_config(file_spec, frame_spec), sink);
+    }
+}
+
+// Unknown file extension.
+TEST(sinks, bad_file_extension) {
+    audio::SampleSpec file_spec;
+    file_spec.clear();
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.bad_ext");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusNoFormat, backend, frame_factory, arena,
+                               "file", file.path(), make_config(file_spec, frame_spec),
+                               sink);
+    }
+}
+
+// Format not supported by backend.
+TEST(sinks, bad_format) {
+    audio::SampleSpec file_spec;
+    CHECK(file_spec.set_custom_format("bad_fmt"));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusNoFormat, backend, frame_factory, arena,
+                               "file", file.path(), make_config(file_spec, frame_spec),
+                               sink);
+    }
+}
+
+// Sub-format not allowed by format.
+TEST(sinks, bad_subformat) {
+    audio::SampleSpec file_spec;
+    file_spec.set_format(audio::Format_Wav);
+    file_spec.set_pcm_subformat(audio::PcmSubformat_SInt18_3_Be);
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISink> sink;
+        test::expect_open_sink(status::StatusBadConfig, backend, frame_factory, arena,
+                               "file", file.path(), make_config(file_spec, frame_spec),
+                               sink);
+    }
+}
+
+} // namespace sndio
+} // namespace roc
diff --git a/src/tests/roc_sndio/test_sources.cpp b/src/tests/roc_sndio/test_sources.cpp
new file mode 100644
index 000000000..20fadd538
--- /dev/null
+++ b/src/tests/roc_sndio/test_sources.cpp
@@ -0,0 +1,499 @@
+/*
+ * Copyright (c) 2023 Roc Streaming authors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <CppUTest/TestHarness.h>
+
+#include "test_helpers/mock_source.h"
+#include "test_helpers/utils.h"
+
+#include "roc_core/heap_arena.h"
+#include "roc_core/macro_helpers.h"
+#include "roc_core/scoped_ptr.h"
+#include "roc_core/slab_pool.h"
+#include "roc_dbgio/temp_file.h"
+#include "roc_sndio/backend_map.h"
+#include "roc_sndio/isink.h"
+
+namespace roc {
+namespace sndio {
+
+namespace {
+
+enum { MaxBufSize = 8192, FrameSize = 500 };
+
+core::HeapArena arena;
+
+core::SlabPool<audio::Frame> frame_pool("frame_pool", arena);
+core::SlabPool<core::Buffer>
+    frame_buffer_pool("frame_buffer_pool",
+                      arena,
+                      sizeof(core::Buffer) + MaxBufSize * sizeof(audio::sample_t));
+
+audio::FrameFactory frame_factory(frame_pool, frame_buffer_pool);
+
+void write_wav(IBackend& backend,
+               const audio::SampleSpec& file_write_spec,
+               const audio::SampleSpec& frame_spec,
+               const char* path,
+               size_t num_samples) {
+    const core::nanoseconds_t frame_len = FrameSize * core::Second
+        / core::nanoseconds_t(frame_spec.sample_rate() * frame_spec.num_channels());
+
+    test::MockSource mock_source(frame_factory, frame_spec, arena);
+    mock_source.add(num_samples * file_write_spec.num_channels());
+
+    IoConfig sink_config;
+    sink_config.sample_spec = file_write_spec;
+    sink_config.frame_length = frame_len;
+
+    IDevice* sink_device = NULL;
+    LONGS_EQUAL(status::StatusOK,
+                backend.open_device(DeviceType_Sink, "file", path, sink_config,
+                                    frame_factory, arena, &sink_device));
+    CHECK(sink_device != NULL);
+
+    core::ScopedPtr<ISink> sink(sink_device->to_sink());
+    CHECK(sink != NULL);
+
+    for (;;) {
+        audio::FramePtr frame =
+            frame_factory.allocate_frame(frame_spec.ns_2_bytes(frame_len));
+
+        const status::StatusCode code = mock_source.read(
+            *frame, frame_spec.ns_2_stream_timestamp(frame_len), audio::ModeHard);
+
+        CHECK(code == status::StatusOK || code == status::StatusPart
+              || code == status::StatusFinish);
+
+        if (code == status::StatusFinish) {
+            break;
+        }
+
+        LONGS_EQUAL(status::StatusOK, sink->write(*frame));
+    }
+}
+
+void expect_read(status::StatusCode expected_code,
+                 ISource& source,
+                 audio::Frame& frame,
+                 packet::stream_timestamp_t requested_samples) {
+    const status::StatusCode code =
+        source.read(frame, requested_samples, audio::ModeHard);
+
+    LONGS_EQUAL(expected_code, code);
+}
+
+audio::ChannelSet make_channel_set(audio::ChannelMask chans) {
+    audio::ChannelSet ch_set;
+    ch_set.set_layout(audio::ChanLayout_Surround);
+    ch_set.set_order(audio::ChanOrder_Smpte);
+    ch_set.set_mask(chans);
+
+    return ch_set;
+}
+
+IoConfig make_config(const audio::SampleSpec& file_read_spec,
+                     const audio::SampleSpec& frame_spec) {
+    IoConfig config;
+    config.sample_spec = file_read_spec;
+    config.frame_length = FrameSize * core::Second
+        / core::nanoseconds_t(frame_spec.sample_rate() * frame_spec.num_channels());
+
+    return config;
+}
+
+} // namespace
+
+TEST_GROUP(sources) {};
+
+// Don't specify input spec.
+TEST(sources, empty_spec) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(44100);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+        write_wav(backend, file_write_spec, frame_spec, file.path(), MaxBufSize * 10);
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "file",
+                                 file.path(), make_config(file_read_spec, frame_spec),
+                                 source);
+
+        audio::SampleSpec actual_spec = source->sample_spec();
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!source->has_state());
+        CHECK(!source->has_latency());
+        CHECK(!source->has_clock());
+        LONGS_EQUAL(status::StatusOK, source->close());
+    }
+}
+
+// Specify only format.
+TEST(sources, explicit_format) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.set_format(audio::Format_Wav);
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(44100);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+        write_wav(backend, file_write_spec, frame_spec, file.path(), MaxBufSize * 10);
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "file",
+                                 file.path(), make_config(file_read_spec, frame_spec),
+                                 source);
+
+        audio::SampleSpec actual_spec = source->sample_spec();
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!source->has_state());
+        CHECK(!source->has_latency());
+        CHECK(!source->has_clock());
+        LONGS_EQUAL(status::StatusOK, source->close());
+    }
+}
+
+// File with non-default rate and channels.
+TEST(sources, non_default_file) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(48000);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Mono));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(48000);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Mono));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+        write_wav(backend, file_write_spec, frame_spec, file.path(), MaxBufSize * 10);
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "file",
+                                 file.path(), make_config(file_read_spec, frame_spec),
+                                 source);
+
+        audio::SampleSpec actual_spec = source->sample_spec();
+        test::expect_specs_equal(backend.name(), frame_spec, actual_spec);
+
+        CHECK(!source->has_state());
+        CHECK(!source->has_latency());
+        CHECK(!source->has_clock());
+        LONGS_EQUAL(status::StatusOK, source->close());
+    }
+}
+
+// File doesn't exist.
+TEST(sources, bad_file_path) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusErrFile, backend, frame_factory, arena,
+                                 "file", "/bad/file.wav",
+                                 make_config(file_read_spec, frame_spec), source);
+    }
+}
+
+// Unknown file extension.
+TEST(sources, bad_file_extension) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.bad_ext");
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusNoFormat, backend, frame_factory, arena,
+                                 "file", file.path(),
+                                 make_config(file_read_spec, frame_spec), source);
+    }
+}
+
+// File is not a valid WAV file.
+TEST(sources, bad_file_contents) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusNoFormat, backend, frame_factory, arena,
+                                 "file", file.path(),
+                                 make_config(file_read_spec, frame_spec), source);
+    }
+}
+
+// Unsupported format.
+TEST(sources, bad_format) {
+    audio::SampleSpec file_read_spec;
+    CHECK(file_read_spec.set_custom_format("bad_fmt"));
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(44100);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+        write_wav(backend, file_write_spec, frame_spec, file.path(), MaxBufSize * 10);
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusNoFormat, backend, frame_factory, arena,
+                                 "file", file.path(),
+                                 make_config(file_read_spec, frame_spec), source);
+    }
+}
+
+// Invalid config.
+TEST(sources, bad_config) {
+    audio::SampleSpec file_read_specs[3];
+    // explicit sub-format not allowed
+    file_read_specs[0].set_format(audio::Format_Wav);
+    file_read_specs[0].set_pcm_subformat(audio::PcmSubformat_Raw);
+    // explicit rate not allowed
+    file_read_specs[1].set_sample_rate(44100);
+    // explicit channels not allowed
+    file_read_specs[2].set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(44100);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_spec = 0; n_spec < ROC_ARRAY_SIZE(file_read_specs); n_spec++) {
+        for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+             n_backend++) {
+            IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+            if (!test::backend_supports_format(backend, arena, "wav")) {
+                continue;
+            }
+
+            dbgio::TempFile file("test.wav");
+            write_wav(backend, file_write_spec, frame_spec, file.path(), MaxBufSize * 10);
+
+            core::ScopedPtr<ISource> source;
+            test::expect_open_source(
+                status::StatusBadConfig, backend, frame_factory, arena, "file",
+                file.path(), make_config(file_read_specs[n_spec], frame_spec), source);
+        }
+    }
+}
+
+// Rewind and read same frame again.
+TEST(sources, rewind) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(44100);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+        write_wav(backend, file_write_spec, frame_spec, file.path(), MaxBufSize * 10);
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "file",
+                                 file.path(), make_config(file_read_spec, frame_spec),
+                                 source);
+
+        audio::FramePtr frame1 = frame_factory.allocate_frame_no_buffer();
+        CHECK(frame1);
+        expect_read(status::StatusOK, *source, *frame1, FrameSize);
+
+        // rewind
+        LONGS_EQUAL(status::StatusOK, source->rewind());
+
+        audio::FramePtr frame2 = frame_factory.allocate_frame_no_buffer();
+        CHECK(frame2);
+        expect_read(status::StatusOK, *source, *frame2, FrameSize);
+
+        LONGS_EQUAL(FrameSize * frame_spec.num_channels(), frame1->num_raw_samples());
+        LONGS_EQUAL(FrameSize * frame_spec.num_channels(), frame2->num_raw_samples());
+
+        if (memcmp(frame1->raw_samples(), frame2->raw_samples(),
+                   frame1->num_raw_samples() * sizeof(audio::sample_t))
+            != 0) {
+            FAIL("frames should be equal");
+        }
+        LONGS_EQUAL(status::StatusOK, source->close());
+    }
+}
+
+// Read until EOF, rewind, repeat.
+TEST(sources, rewind_after_eof) {
+    audio::SampleSpec file_read_spec;
+    file_read_spec.clear();
+
+    audio::SampleSpec file_write_spec;
+    file_write_spec.set_format(audio::Format_Wav);
+    file_write_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    file_write_spec.set_sample_rate(44100);
+    file_write_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    audio::SampleSpec frame_spec;
+    frame_spec.set_format(audio::Format_Pcm);
+    frame_spec.set_pcm_subformat(audio::PcmSubformat_Raw);
+    frame_spec.set_sample_rate(44100);
+    frame_spec.set_channel_set(make_channel_set(audio::ChanMask_Surround_Stereo));
+
+    for (size_t n_backend = 0; n_backend < BackendMap::instance().num_backends();
+         n_backend++) {
+        IBackend& backend = BackendMap::instance().nth_backend(n_backend);
+        if (!test::backend_supports_format(backend, arena, "wav")) {
+            continue;
+        }
+
+        dbgio::TempFile file("test.wav");
+        write_wav(backend, file_write_spec, frame_spec, file.path(), FrameSize * 2);
+
+        core::ScopedPtr<ISource> source;
+        test::expect_open_source(status::StatusOK, backend, frame_factory, arena, "file",
+                                 file.path(), make_config(file_read_spec, frame_spec),
+                                 source);
+
+        audio::FramePtr frame = frame_factory.allocate_frame_no_buffer();
+        CHECK(frame);
+
+        for (int i = 0; i < 10; i++) {
+            expect_read(status::StatusOK, *source, *frame, FrameSize);
+            expect_read(status::StatusOK, *source, *frame, FrameSize);
+            expect_read(status::StatusFinish, *source, *frame, FrameSize);
+
+            // rewind
+            LONGS_EQUAL(status::StatusOK, source->rewind());
+        }
+        LONGS_EQUAL(status::StatusOK, source->close());
+    }
+}
+
+} // namespace sndio
+
+} // namespace roc
diff --git a/src/tools/roc_copy/cmdline.ggo b/src/tools/roc_copy/cmdline.ggo
index f1c094228..d087ff7e4 100644
--- a/src/tools/roc_copy/cmdline.ggo
+++ b/src/tools/roc_copy/cmdline.ggo
@@ -11,10 +11,9 @@ option "list-supported" L "List supported protocols, formats, etc." optional
 section "I/O options"
 
     option "input" i "Input file URI" typestr="FILE_URI" string optional
-    option "input-format" - "Force input file format" typestr="FILE_FORMAT" string optional
+    option "input-encoding" - "Input file encoding" typestr="IO_ENCODING" string optional
 
     option "output" o "Output file URI" typestr="FILE_URI" string optional
-    option "output-format" - "Force output file format" typestr="FILE_FORMAT" string optional
     option "output-encoding" - "Output file encoding" typestr="IO_ENCODING" string optional
 
     option "io-frame-len" - "I/O frame length, TIME units" typestr="TIME" string optional
diff --git a/src/tools/roc_copy/main.cpp b/src/tools/roc_copy/main.cpp
index 5067681fb..f1d1555c6 100644
--- a/src/tools/roc_copy/main.cpp
+++ b/src/tools/roc_copy/main.cpp
@@ -120,8 +120,9 @@ bool build_transcoder_config(const gengetopt_args_info& args,
 
 size_t compute_max_frame_size(const sndio::IoConfig& io_config) {
     audio::SampleSpec spec = io_config.sample_spec;
-    spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                      audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000);
+    spec.use_defaults(audio::Format_Pcm, audio::PcmSubformat_Raw,
+                      audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
+                      audio::ChanMask_Surround_7_1_4, 48000);
 
     return spec.ns_2_samples_overall(io_config.frame_length) * sizeof(audio::sample_t);
 }
@@ -142,11 +143,6 @@ bool parse_input_uri(const gengetopt_args_info& args, address::IoUri& input_uri)
         return false;
     }
 
-    if (!args.input_format_given && input_uri.is_special_file()) {
-        roc_log(LogError, "--input-format should be specified if --input is \"-\"");
-        return false;
-    }
-
     return true;
 }
 
@@ -161,9 +157,11 @@ bool parse_output_uri(const gengetopt_args_info& args, address::IoUri& output_ur
         return false;
     }
 
-    if (!args.output_format_given && output_uri.is_special_file()) {
-        roc_log(LogError, "--output-format should be specified if --output is \"-\"");
-        return false;
+    if (output_uri.is_special_file()) {
+        if (!args.output_encoding_given) {
+            roc_log(LogError, "--output-encoding is required when --output is \"-\"");
+            return false;
+        }
     }
 
     return true;
@@ -172,10 +170,9 @@ bool parse_output_uri(const gengetopt_args_info& args, address::IoUri& output_ur
 bool open_input_source(sndio::BackendDispatcher& backend_dispatcher,
                        const sndio::IoConfig& io_config,
                        const address::IoUri& input_uri,
-                       const char* input_format,
                        core::ScopedPtr<sndio::ISource>& input_source) {
     const status::StatusCode code =
-        backend_dispatcher.open_source(input_uri, input_format, io_config, input_source);
+        backend_dispatcher.open_source(input_uri, io_config, input_source);
 
     if (code != status::StatusOK) {
         roc_log(LogError, "can't open --input file or device: status=%s",
@@ -194,10 +191,9 @@ bool open_input_source(sndio::BackendDispatcher& backend_dispatcher,
 bool open_output_sink(sndio::BackendDispatcher& backend_dispatcher,
                       const sndio::IoConfig& io_config,
                       const address::IoUri& output_uri,
-                      const char* output_format,
                       core::ScopedPtr<sndio::ISink>& output_sink) {
     const status::StatusCode code =
-        backend_dispatcher.open_sink(output_uri, output_format, io_config, output_sink);
+        backend_dispatcher.open_sink(output_uri, io_config, output_sink);
 
     if (code != status::StatusOK) {
         roc_log(LogError, "can't open --output file or device: status=%s",
@@ -258,12 +254,12 @@ int main(int argc, char** argv) {
     }
 
     core::ScopedPtr<sndio::ISource> input_source;
-    if (!open_input_source(backend_dispatcher, input_config, input_uri,
-                           args.input_format_arg, input_source)) {
+    if (!open_input_source(backend_dispatcher, input_config, input_uri, input_source)) {
         return 1;
     }
 
     input_config.sample_spec = input_source->sample_spec();
+    input_config.frame_length = input_source->frame_length();
 
     sndio::IoConfig output_config;
     if (!build_output_config(args, input_config, output_config)) {
@@ -280,7 +276,7 @@ int main(int argc, char** argv) {
     core::ScopedPtr<sndio::ISink> output_sink;
     if (args.output_given) {
         if (!open_output_sink(backend_dispatcher, output_config, output_uri,
-                              args.output_format_arg, output_sink)) {
+                              output_sink)) {
             return 1;
         }
         output_config.sample_spec = output_sink->sample_spec();
diff --git a/src/tools/roc_recv/cmdline.ggo b/src/tools/roc_recv/cmdline.ggo
index 8a12ab6f8..2224e44ed 100644
--- a/src/tools/roc_recv/cmdline.ggo
+++ b/src/tools/roc_recv/cmdline.ggo
@@ -15,7 +15,6 @@ section "Operation options"
 section "Output options"
 
     option "output" o "Output file or device URI" typestr="IO_URI" string optional
-    option "output-format" - "Force output file format" typestr="FILE_FORMAT" string optional
 
     option "io-encoding" - "Output device encoding" typestr="IO_ENCODING" string optional
     option "io-latency" - "Output device latency, TIME units" typestr="TIME"string optional
@@ -24,10 +23,8 @@ section "Output options"
 section "Backup input options"
 
     option "backup" -
-        "Backup file or device URI (used as input when there are no connections)"
+        "Backup file URI (used as input when there are no connections)"
         typestr="IO_URI" string optional
-    option "backup-format" - "Force backup file format"
-        typestr="FILE_FORMAT" string optional
 
 section "Network options"
 
diff --git a/src/tools/roc_recv/main.cpp b/src/tools/roc_recv/main.cpp
index a39f9394c..70da9886c 100644
--- a/src/tools/roc_recv/main.cpp
+++ b/src/tools/roc_recv/main.cpp
@@ -105,10 +105,15 @@ bool build_context_config(const gengetopt_args_info& args,
         }
     } else {
         audio::SampleSpec spec = io_config.sample_spec;
-        spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                          audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000);
+        spec.use_defaults(audio::Format_Pcm, audio::PcmSubformat_Raw,
+                          audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
+                          audio::ChanMask_Surround_7_1_4, 48000);
+        core::nanoseconds_t len = io_config.frame_length;
+        if (len == 0) {
+            len = 10 * core::Millisecond;
+        }
         context_config.max_frame_size =
-            spec.ns_2_samples_overall(io_config.frame_length) * sizeof(audio::sample_t);
+            spec.ns_2_samples_overall(len) * sizeof(audio::sample_t);
     }
 
     return true;
@@ -293,13 +298,6 @@ bool build_receiver_config(const gengetopt_args_info& args,
             roc_log(LogError, "invalid --max-latency: should be > 0");
             return false;
         }
-        if (receiver_config.session_defaults.latency.min_target_latency
-            > receiver_config.session_defaults.latency.max_target_latency) {
-            roc_log(
-                LogError,
-                "incorrect --max-latency: should be greater or equal to --min-latency");
-            return false;
-        }
     }
 
     if (args.no_play_timeout_given) {
@@ -337,7 +335,7 @@ bool build_receiver_config(const gengetopt_args_info& args,
     receiver_config.common.enable_cpu_clock = !output_sink.has_clock();
     receiver_config.common.output_sample_spec = output_sink.sample_spec();
 
-    if (!receiver_config.common.output_sample_spec.is_valid()) {
+    if (!receiver_config.common.output_sample_spec.is_complete()) {
         roc_log(LogError,
                 "can't detect output encoding, try to set it"
                 " explicitly with --io-encoding option");
@@ -353,18 +351,11 @@ bool parse_output_uri(const gengetopt_args_info& args, address::IoUri& output_ur
             roc_log(LogError, "invalid --output file or device URI");
             return false;
         }
-    }
-
-    if (args.output_format_given) {
-        if (output_uri.is_valid() && !output_uri.is_file()) {
-            roc_log(LogError,
-                    "--output-format can't be used if --output is not a file URI");
-            return false;
-        }
-    } else {
         if (output_uri.is_special_file()) {
-            roc_log(LogError, "--output-format should be specified if --output is \"-\"");
-            return false;
+            if (!args.io_encoding_given) {
+                roc_log(LogError, "--io-encoding is required when --output is \"-\"");
+                return false;
+            }
         }
     }
 
@@ -374,11 +365,10 @@ bool parse_output_uri(const gengetopt_args_info& args, address::IoUri& output_ur
 bool open_output_sink(sndio::BackendDispatcher& backend_dispatcher,
                       const sndio::IoConfig& io_config,
                       const address::IoUri& output_uri,
-                      const char* output_format,
                       core::ScopedPtr<sndio::ISink>& output_sink) {
     if (output_uri.is_valid()) {
-        const status::StatusCode code = backend_dispatcher.open_sink(
-            output_uri, output_format, io_config, output_sink);
+        const status::StatusCode code =
+            backend_dispatcher.open_sink(output_uri, io_config, output_sink);
 
         if (code != status::StatusOK) {
             roc_log(LogError, "can't open --output file or device: status=%s",
@@ -401,21 +391,18 @@ bool open_output_sink(sndio::BackendDispatcher& backend_dispatcher,
 
 bool parse_backup_uri(const gengetopt_args_info& args, address::IoUri& backup_uri) {
     if (!address::parse_io_uri(args.backup_arg, backup_uri)) {
-        roc_log(LogError, "invalid --backup file or device URI");
+        roc_log(LogError, "invalid --backup URI: bad format");
         return false;
     }
 
-    if (args.backup_format_given) {
-        if (backup_uri.is_valid() && !backup_uri.is_file()) {
-            roc_log(LogError,
-                    "--backup-format can't be used if --backup is not a file URI");
-            return false;
-        }
-    } else {
-        if (backup_uri.is_special_file()) {
-            roc_log(LogError, "--backup-format should be specified if --backup is \"-\"");
-            return false;
-        }
+    if (!backup_uri.is_file()) {
+        roc_log(LogError, "invalid --backup URI: should be file");
+        return false;
+    }
+
+    if (backup_uri.is_special_file()) {
+        roc_log(LogError, "invalid --backup URI: can't be \"-\"");
+        return false;
     }
 
     return true;
@@ -424,10 +411,9 @@ bool parse_backup_uri(const gengetopt_args_info& args, address::IoUri& backup_ur
 bool open_backup_source(sndio::BackendDispatcher& backend_dispatcher,
                         const sndio::IoConfig& io_config,
                         const address::IoUri& backup_uri,
-                        const char* backup_format,
                         core::ScopedPtr<sndio::ISource>& backup_source) {
-    const status::StatusCode code = backend_dispatcher.open_source(
-        backup_uri, backup_format, io_config, backup_source);
+    const status::StatusCode code =
+        backend_dispatcher.open_source(backup_uri, io_config, backup_source);
 
     if (code != status::StatusOK) {
         roc_log(LogError, "can't open --backup file or device: status=%s",
@@ -452,11 +438,11 @@ bool open_backup_transcoder(
 
     transcoder_config.input_sample_spec =
         audio::SampleSpec(backup_source.sample_spec().sample_rate(),
-                          receiver_config.common.output_sample_spec.pcm_format(),
+                          receiver_config.common.output_sample_spec.pcm_subformat(),
                           receiver_config.common.output_sample_spec.channel_set());
     transcoder_config.output_sample_spec =
         audio::SampleSpec(receiver_config.common.output_sample_spec.sample_rate(),
-                          receiver_config.common.output_sample_spec.pcm_format(),
+                          receiver_config.common.output_sample_spec.pcm_subformat(),
                           receiver_config.common.output_sample_spec.channel_set());
 
     backup_transcoder.reset(new (context.arena()) pipeline::TranscoderSource(
@@ -665,12 +651,12 @@ int main(int argc, char** argv) {
     }
 
     core::ScopedPtr<sndio::ISink> output_sink;
-    if (!open_output_sink(backend_dispatcher, io_config, output_uri,
-                          args.output_format_arg, output_sink)) {
+    if (!open_output_sink(backend_dispatcher, io_config, output_uri, output_sink)) {
         return 1;
     }
 
     io_config.sample_spec = output_sink->sample_spec();
+    io_config.frame_length = output_sink->frame_length();
 
     pipeline::ReceiverSourceConfig receiver_config;
     if (!build_receiver_config(args, receiver_config, context, *output_sink)) {
@@ -687,7 +673,7 @@ int main(int argc, char** argv) {
         }
 
         if (!open_backup_source(backend_dispatcher, io_config, backup_uri,
-                                args.backup_format_arg, backup_source)) {
+                                backup_source)) {
             return 1;
         }
 
diff --git a/src/tools/roc_send/cmdline.ggo b/src/tools/roc_send/cmdline.ggo
index 8359db854..6e16b454a 100644
--- a/src/tools/roc_send/cmdline.ggo
+++ b/src/tools/roc_send/cmdline.ggo
@@ -11,7 +11,6 @@ option "list-supported" L "List supported protocols, formats, etc." optional
 section "Input options"
 
     option "input" i "Input file or device URI" typestr="IO_URI" string optional
-    option "input-format" - "Force input file format" typestr="FILE_FORMAT" string optional
 
     option "io-encoding" - "Input device encoding" typestr="IO_ENCODING" string optional
     option "io-latency" - "Input device latency, TIME units" typestr="TIME"string optional
diff --git a/src/tools/roc_send/main.cpp b/src/tools/roc_send/main.cpp
index 7b8879318..f759efebe 100644
--- a/src/tools/roc_send/main.cpp
+++ b/src/tools/roc_send/main.cpp
@@ -93,10 +93,15 @@ bool build_context_config(const gengetopt_args_info& args,
         }
     } else {
         audio::SampleSpec spec = io_config.sample_spec;
-        spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround,
-                          audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000);
-        context_config.max_packet_size = packet::Packet::approx_size(
-            spec.ns_2_samples_overall(io_config.frame_length));
+        spec.use_defaults(audio::Format_Pcm, audio::PcmSubformat_Raw,
+                          audio::ChanLayout_Surround, audio::ChanOrder_Smpte,
+                          audio::ChanMask_Surround_7_1_4, 48000);
+        core::nanoseconds_t len = io_config.frame_length;
+        if (len == 0) {
+            len = 10 * core::Millisecond;
+        }
+        context_config.max_packet_size =
+            packet::Packet::approx_size(spec.ns_2_samples_overall(len));
     }
 
     if (args.max_frame_size_given) {
@@ -349,13 +354,6 @@ bool build_sender_config(const gengetopt_args_info& args,
             roc_log(LogError, "invalid --max-latency: should be > 0");
             return false;
         }
-        if (sender_config.latency.min_target_latency
-            > sender_config.latency.max_target_latency) {
-            roc_log(
-                LogError,
-                "incorrect --max-latency: should be greater or equal to --min-latency");
-            return false;
-        }
     }
 
     sender_config.enable_profiling = args.prof_flag;
@@ -367,7 +365,7 @@ bool build_sender_config(const gengetopt_args_info& args,
     sender_config.enable_cpu_clock = !input_source.has_clock();
     sender_config.input_sample_spec = input_source.sample_spec();
 
-    if (!sender_config.input_sample_spec.is_valid()) {
+    if (!sender_config.input_sample_spec.is_complete()) {
         roc_log(LogError,
                 "can't detect input encoding, try to set it "
                 "explicitly with --io-encoding option");
@@ -385,30 +383,16 @@ bool parse_input_uri(const gengetopt_args_info& args, address::IoUri& input_uri)
         }
     }
 
-    if (args.input_format_given) {
-        if (input_uri.is_valid() && !input_uri.is_file()) {
-            roc_log(LogError,
-                    "--input-format can't be used if --input is not a file URI");
-            return false;
-        }
-    } else {
-        if (input_uri.is_special_file()) {
-            roc_log(LogError, "--input-format should be specified if --input is \"-\"");
-            return false;
-        }
-    }
-
     return true;
 }
 
 bool open_input_source(sndio::BackendDispatcher& backend_dispatcher,
                        const sndio::IoConfig& io_config,
                        const address::IoUri& input_uri,
-                       const char* input_format,
                        core::ScopedPtr<sndio::ISource>& input_source) {
     if (input_uri.is_valid()) {
-        const status::StatusCode code = backend_dispatcher.open_source(
-            input_uri, input_format, io_config, input_source);
+        const status::StatusCode code =
+            backend_dispatcher.open_source(input_uri, io_config, input_source);
 
         if (code != status::StatusOK) {
             roc_log(LogError, "can't open --input file or device: status=%s",
@@ -605,12 +589,12 @@ int main(int argc, char** argv) {
     }
 
     core::ScopedPtr<sndio::ISource> input_source;
-    if (!open_input_source(backend_dispatcher, io_config, input_uri,
-                           args.input_format_arg, input_source)) {
+    if (!open_input_source(backend_dispatcher, io_config, input_uri, input_source)) {
         return 1;
     }
 
     io_config.sample_spec = input_source->sample_spec();
+    io_config.frame_length = input_source->frame_length();
 
     pipeline::SenderSinkConfig sender_config;
     if (!build_sender_config(args, sender_config, context, *input_source)) {