Skip to content

Commit

Permalink
roc-streaminggh-608: Support custom format in wav sink
Browse files Browse the repository at this point in the history
  • Loading branch information
gavv committed Aug 20, 2024
1 parent 6198f5c commit ed608e1
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 78 deletions.
70 changes: 45 additions & 25 deletions src/internal_modules/roc_sndio/wav_header.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,43 @@
namespace roc {
namespace sndio {

WavHeader::WavHeader(uint16_t num_channels,
uint32_t sample_rate,
uint16_t bits_per_sample)
WavHeader::WavHeader(const uint16_t format_tag,
const uint16_t bits_per_sample,
const uint32_t sample_rate,
const uint16_t num_channels)
: data_(
// chunk_id
core::EndianOps::swap_native_be<uint32_t>(0x52494646), // {'R','I','F','F'}
// chunk_size
0,
// form_type
core::EndianOps::swap_native_be<uint32_t>(0x57415645), // {'W','A','V','E'}
// subchunk1_id
core::EndianOps::swap_native_be<uint32_t>(0x666d7420), // {'f','m','t',' '}
core::EndianOps::swap_native_le<uint16_t>(0x10), // 16
core::EndianOps::swap_native_le<uint16_t>(0x3), // IEEE Float
// subchunk1_size
core::EndianOps::swap_native_le<uint16_t>(16),
// audio_format
core::EndianOps::swap_native_le<uint16_t>(format_tag),
// num_channels
core::EndianOps::swap_native_le(num_channels),
// sample_rate
core::EndianOps::swap_native_le<uint32_t>(sample_rate),
// byte_rate
core::EndianOps::swap_native_le<uint32_t>(sample_rate * num_channels
* (bits_per_sample / 8u)),
// block_align
core::EndianOps::swap_native_le<uint16_t>(num_channels * (bits_per_sample / 8u)),
// bits_per_sample
core::EndianOps::swap_native_le<uint16_t>(bits_per_sample),
core::EndianOps::swap_native_be<uint32_t>(0x64617461) /* {'d','a','t','a'} */) {
// subchunk2_id
core::EndianOps::swap_native_be<uint32_t>(0x64617461), // {'d','a','t','a'}
// subchunk2_size
0) {
}

WavHeader::WavHeaderData::WavHeaderData(uint32_t chunk_id,
uint32_t format,
uint32_t chunk_size,
uint32_t form_type,
uint32_t subchunk1_id,
uint32_t subchunk1_size,
uint16_t audio_format,
Expand All @@ -40,30 +57,33 @@ WavHeader::WavHeaderData::WavHeaderData(uint32_t chunk_id,
uint32_t byte_rate,
uint16_t block_align,
uint16_t bits_per_sample,
uint32_t subchunk2_id)
: chunk_id_(chunk_id)
, format_(format)
, subchunk1_id_(subchunk1_id)
, subchunk1_size_(subchunk1_size)
, audio_format_(audio_format)
, num_channels_(num_channels)
, sample_rate_(sample_rate)
, byte_rate_(byte_rate)
, block_align_(block_align)
, bits_per_sample_(bits_per_sample)
, subchunk2_id_(subchunk2_id) {
uint32_t subchunk2_id,
uint32_t subchunk2_size)
: chunk_id(chunk_id)
, chunk_size(chunk_size)
, form_type(form_type)
, subchunk1_id(subchunk1_id)
, subchunk1_size(subchunk1_size)
, audio_format(audio_format)
, num_channels(num_channels)
, sample_rate(sample_rate)
, byte_rate(byte_rate)
, block_align(block_align)
, bits_per_sample(bits_per_sample)
, subchunk2_id(subchunk2_id)
, subchunk2_size(subchunk2_size) {
}

uint16_t WavHeader::num_channels() const {
return data_.num_channels_;
return data_.num_channels;
}

uint32_t WavHeader::sample_rate() const {
return data_.sample_rate_;
return data_.sample_rate;
}

uint16_t WavHeader::bits_per_sample() const {
return data_.bits_per_sample_;
return data_.bits_per_sample;
}

void WavHeader::reset_sample_counter(uint32_t num_samples) {
Expand All @@ -72,9 +92,9 @@ void WavHeader::reset_sample_counter(uint32_t num_samples) {

const WavHeader::WavHeaderData& WavHeader::update_and_get_header(uint32_t num_samples) {
num_samples_ += num_samples;
data_.subchunk2_size_ =
num_samples_ * data_.num_channels_ * (data_.bits_per_sample_ / 8u);
data_.chunk_size_ = METADATA_SIZE_ + data_.subchunk2_size_;
data_.subchunk2_size =
num_samples_ * data_.num_channels * (data_.bits_per_sample / 8u);
data_.chunk_size = METADATA_SIZE_ + data_.subchunk2_size;

return data_;
}
Expand Down
52 changes: 33 additions & 19 deletions src/internal_modules/roc_sndio/wav_header.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@
namespace roc {
namespace sndio {

//! WAV header
//! PCM signed integers.
const uint16_t WAV_FORMAT_PCM = 0x0001;
//! PCM IEEE floats.
const uint16_t WAV_FORMAT_IEEE_FLOAT = 0x0003;

//! WAV header.
//! @remarks
//! Holds data of a WAV header
//! Allows easy generation of WAV header
//! Holds data of a WAV header.
//! Allows easy generation of WAV header.
//! Reference:
//! https://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
class WavHeader {
public:
//! WAV header data
ROC_ATTR_PACKED_BEGIN struct WavHeaderData {
//! Constructor
WavHeaderData(uint32_t chunk_id,
uint32_t format,
uint32_t chunk_size,
uint32_t form_type,
uint32_t subchunk1_id,
uint32_t subchunk1_size,
uint16_t audio_format,
Expand All @@ -37,40 +45,46 @@ class WavHeader {
uint32_t byte_rate,
uint16_t block_align,
uint16_t bits_per_sample,
uint32_t subchunk2_id);
uint32_t subchunk2_id,
uint32_t subchunk2_size);
// RIFF header
//! Chunk ID
const uint32_t chunk_id_;
const uint32_t chunk_id;
//! Chunk size
uint32_t chunk_size_;
uint32_t chunk_size;
//! Format
const uint32_t format_;
const uint32_t form_type;

// WAVE fmt
//! Subchunk1 ID
const uint32_t subchunk1_id_;
const uint32_t subchunk1_id;
//! Subchunk1 size
const uint32_t subchunk1_size_;
const uint32_t subchunk1_size;
//! Audio format
const uint16_t audio_format_;
const uint16_t audio_format;
//! Num channels
const uint16_t num_channels_;
const uint16_t num_channels;
//! Sample rate
const uint32_t sample_rate_;
const uint32_t sample_rate;
//! Byte rate
const uint32_t byte_rate_;
const uint32_t byte_rate;
//! Block align
const uint16_t block_align_;
const uint16_t block_align;
//! Bits per sample
const uint16_t bits_per_sample_;
const uint16_t bits_per_sample;

// WAVE data
//! Subchunk2 ID
const uint32_t subchunk2_id_;
const uint32_t subchunk2_id;
//! Subchunk2 size
uint32_t subchunk2_size_;
uint32_t subchunk2_size;
} ROC_ATTR_PACKED_END;

//! Initialize
WavHeader(uint16_t num_channels, uint32_t sample_rate, uint16_t bits_per_sample);
WavHeader(const uint16_t format_tag,
const uint16_t bits_per_sample,
const uint32_t sample_rate,
const uint16_t num_channels);

//! Get number of channels
uint16_t num_channels() const;
Expand Down
111 changes: 77 additions & 34 deletions src/internal_modules/roc_sndio/wav_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "roc_sndio/wav_sink.h"
#include "roc_audio/pcm_format.h"
#include "roc_audio/sample_format.h"
#include "roc_audio/sample_spec_to_str.h"
#include "roc_core/endian_ops.h"
#include "roc_core/log.h"
#include "roc_core/panic.h"
Expand All @@ -23,6 +24,7 @@ WavSink::WavSink(audio::FrameFactory& frame_factory,
: 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");
Expand All @@ -36,16 +38,51 @@ WavSink::WavSink(audio::FrameFactory& frame_factory,
audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo,
44100);

if (!sample_spec_.is_raw()) {
roc_log(LogError, "wav sink: sample format can be only \"-\" or \"%s\"",
audio::pcm_format_to_str(audio::Sample_RawFormat));
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 fmt = audio::pcm_format_traits(sample_spec_.pcm_format());

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 (!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;
}

// 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;
}

if (sample_spec_.pcm_format() == fmt.default_variant) {
sample_spec_.set_pcm_format(fmt.le_variant);
}

const uint16_t fmt_code =
fmt.has_flags(audio::Pcm_IsInteger) ? WAV_FORMAT_PCM : WAV_FORMAT_IEEE_FLOAT;

header_.reset(new (header_)
WavHeader(sample_spec_.num_channels(), sample_spec_.sample_rate(),
sizeof(audio::sample_t) * 8));
WavHeader(fmt_code, fmt.bit_width, sample_spec_.sample_rate(),
sample_spec_.num_channels()));

init_status_ = status::StatusOK;
}
Expand Down Expand Up @@ -102,43 +139,49 @@ status::StatusCode WavSink::write(audio::Frame& frame) {
roc_panic("wav sink: not opened");
}

const audio::sample_t* samples = frame.raw_samples();
size_t n_samples = frame.num_raw_samples();

if (n_samples > 0) {
if (fseek(output_file_, 0, SEEK_SET) != 0) {
roc_log(LogError, "wav sink: failed to seek to the beginning of the file: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}

const WavHeader::WavHeaderData& wav_header =
header_->update_and_get_header(n_samples);
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) {
roc_log(LogError, "wav sink: failed to write header: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}
is_first_ = false;
}

if (fseek(output_file_, 0, SEEK_END) != 0) {
roc_log(LogError,
"wav sink: failed to seek to append position of the file: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}
// First append samples to file.
if (fwrite(frame.bytes(), 1, frame.num_bytes(), output_file_) != frame.num_bytes()) {
roc_log(LogError, "wav sink: failed to write samples: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}

if (fwrite(samples, sizeof(audio::sample_t), n_samples, output_file_)
!= n_samples) {
roc_log(LogError, "wav sink: failed to write samples: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}
if (fseek(output_file_, 0, SEEK_SET) != 0) {
roc_log(LogError, "wav sink: failed to seek to the beginning of file: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}

if (fflush(output_file_) != 0) {
roc_log(LogError, "wav sink: failed to flush data to the file: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}
// Then update header so that someone who is reading the file concurrently
// could process the appended samples.
const WavHeader::WavHeaderData& wav_header =
header_->update_and_get_header(frame.duration());
if (fwrite(&wav_header, sizeof(wav_header), 1, output_file_) != 1) {
roc_log(LogError, "wav sink: failed to write header: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}

if (fseek(output_file_, 0, SEEK_END) != 0) {
roc_log(LogError, "wav sink: failed to seek to the end of file: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}

if (fflush(output_file_) != 0) {
roc_log(LogError, "wav sink: failed to flush data to the file: %s",
core::errno_to_str(errno).c_str());
return status::StatusErrFile;
}

return status::StatusOK;
Expand Down
1 change: 1 addition & 0 deletions src/internal_modules/roc_sndio/wav_sink.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class WavSink : public ISink, public core::NonCopyable<> {

FILE* output_file_;
core::Optional<WavHeader> header_;
bool is_first_;

status::StatusCode init_status_;
};
Expand Down

0 comments on commit ed608e1

Please sign in to comment.