blob: 9a42e057a6a490d2aad447229e8d433944228a89 [file] [log] [blame]
/*
* Copyright 2019 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
* www.ehima.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This module contains API of the audio stream control protocol.
*/
#include "client_parser.h"
#include <base/strings/string_number_conversions.h>
#include <endian.h>
#include <hardware/bt_common_types.h>
#include <map>
#include <memory>
#include <numeric>
#include "bta_le_audio_api.h"
#include "gap_api.h"
#include "le_audio_types.h"
#include "osi/include/allocator.h"
using le_audio::types::acs_ac_record;
namespace le_audio {
namespace client_parser {
namespace ascs {
static std::map<uint8_t, std::string> ase_state_map_string = {
{kAseStateIdle, "Idle"},
{kAseStateCodecConfigured, "Codec Configured"},
{kAseStateQosConfigured, "QoS Configured"},
{kAseStateEnabling, "Enabling"},
{kAseStateStreaming, "Streaming"},
{kAseStateDisabling, "Disabling"},
{kAseStateReleasing, "Releasing"},
};
static std::map<uint8_t, std::string> ctp_opcode_map_string = {
{kCtpOpcodeCodecConfiguration, "Config Codec"},
{kCtpOpcodeQosConfiguration, "Config QoS"},
{kCtpOpcodeEnable, "Enable"},
{kCtpOpcodeReceiverStartReady, "Receiver Start Ready"},
{kCtpOpcodeDisable, "Disable"},
{kCtpOpcodeReceiverStopReady, "Receiver Stop Ready"},
{kCtpOpcodeUpdateMetadata, "Update Metadata"},
{kCtpOpcodeRelease, "Release"},
};
static std::map<uint8_t, std::string> ctp_reason_map_string = {
{kCtpResponseNoReason, ""},
{kCtpResponseCodecId, "Codec ID"},
{kCtpResponseCodecSpecificConfiguration, "Codec specific configuration"},
{kCtpResponseSduInterval, "SDU interval"},
{kCtpResponseFraming, "Framing"},
{kCtpResponsePhy, "PHY"},
{kCtpResponseMaximumSduSize, "Maximum SDU size"},
{kCtpResponseRetransmissionNumber, "Retransmission number"},
{kCtpResponseMaxTransportLatency, "Max Transport latency"},
{kCtpResponsePresentationDelay, "Presentation delay"},
{kCtpResponseInvalidAseCisMapping, "Invalid ASE CIS mapping"},
};
static std::map<uint8_t, std::string> ctp_response_code_map_string = {
{kCtpResponseCodeSuccess, "Success"},
{kCtpResponseCodeUnsupportedOpcode, "Unsupported Opcode"},
{kCtpResponseCodeInvalidLength, "Invalid Length"},
{kCtpResponseCodeInvalidAseId, "Invalid ASE ID"},
{kCtpResponseCodeInvalidAseStateMachineTransition,
"Invalid ASE State Machine Transition"},
{kCtpResponseCodeInvalidAseDirection, "Invalid ASE Direction"},
{kCtpResponseCodeUnsupportedAudioCapabilities,
"Unsupported Audio Capabilities"},
{kCtpResponseCodeUnsupportedConfigurationParameterValue,
"Unsupported Configuration Parameter Value"},
{kCtpResponseCodeRejectedConfigurationParameterValue,
"Rejected Configuration Parameter Value"},
{kCtpResponseCodeInvalidConfigurationParameterValue,
"Invalid Configuration Parameter Value"},
{kCtpResponseCodeUnsupportedMetadata, "Unsupported Metadata"},
{kCtpResponseCodeRejectedMetadata, "Rejected Metadata"},
{kCtpResponseCodeInvalidMetadata, "Invalid Metadata"},
{kCtpResponseCodeInsufficientResources, "Insufficient Resources"},
{kCtpResponseCodeUnspecifiedError, "Unspecified Error"},
};
bool ParseAseStatusHeader(ase_rsp_hdr& arh, uint16_t len,
const uint8_t* value) {
if (len < kAseRspHdrMinLen) {
LOG(ERROR) << __func__
<< ", wrong len of ASE char (header): " << static_cast<int>(len);
return false;
}
STREAM_TO_UINT8(arh.id, value);
STREAM_TO_UINT8(arh.state, value);
LOG(INFO) << "ASE status: "
<< "\tASE id: " << loghex(arh.id)
<< "\tASE state: " << ase_state_map_string[arh.state] << " ("
<< loghex(arh.state) << ")";
return true;
}
bool ParseAseStatusCodecConfiguredStateParams(
struct ase_codec_configured_state_params& rsp, uint16_t len,
const uint8_t* value) {
uint8_t codec_spec_conf_len;
if (len < kAseStatusCodecConfMinLen) {
LOG(ERROR) << "Wrong len of codec conf status (Codec conf header)";
return false;
}
STREAM_TO_UINT8(rsp.framing, value);
STREAM_TO_UINT8(rsp.preferred_phy, value);
STREAM_TO_UINT8(rsp.preferred_retrans_nb, value);
STREAM_TO_UINT16(rsp.max_transport_latency, value);
STREAM_TO_UINT24(rsp.pres_delay_min, value);
STREAM_TO_UINT24(rsp.pres_delay_max, value);
STREAM_TO_UINT24(rsp.preferred_pres_delay_min, value);
STREAM_TO_UINT24(rsp.preferred_pres_delay_max, value);
STREAM_TO_UINT8(rsp.codec_id.coding_format, value);
STREAM_TO_UINT16(rsp.codec_id.vendor_company_id, value);
STREAM_TO_UINT16(rsp.codec_id.vendor_codec_id, value);
STREAM_TO_UINT8(codec_spec_conf_len, value);
len -= kAseStatusCodecConfMinLen;
if (len != codec_spec_conf_len) {
LOG(ERROR) << "Wrong len of codec conf status (Codec spec conf)";
return false;
}
if (codec_spec_conf_len)
rsp.codec_spec_conf =
std::vector<uint8_t>(value, value + codec_spec_conf_len);
LOG(INFO) << __func__ << ", Codec configuration"
<< "\n\tFraming: " << loghex(rsp.framing)
<< "\n\tPreferred PHY: " << loghex(rsp.preferred_phy)
<< "\n\tPreferred retransmission number: "
<< loghex(rsp.preferred_retrans_nb) << "\n\tMax transport latency: "
<< loghex(rsp.max_transport_latency)
<< "\n\tPresence delay min: " << loghex(rsp.pres_delay_min)
<< "\n\tPresence delay max: " << loghex(rsp.pres_delay_max)
<< "\n\tPreferredPresentationDelayMin: "
<< loghex(rsp.preferred_pres_delay_min)
<< "\n\tPreferredPresentationDelayMax: "
<< loghex(rsp.preferred_pres_delay_max)
<< "\n\tCoding format: " << loghex(rsp.codec_id.coding_format)
<< "\n\tVendor codec company ID: "
<< loghex(rsp.codec_id.vendor_company_id)
<< "\n\tVendor codec ID: " << loghex(rsp.codec_id.vendor_codec_id)
<< "\n\tCodec specific conf len: " << (int)codec_spec_conf_len
<< "\n\tCodec specific conf: "
<< base::HexEncode(rsp.codec_spec_conf.data(),
rsp.codec_spec_conf.size());
return true;
}
bool ParseAseStatusQosConfiguredStateParams(
struct ase_qos_configured_state_params& rsp, uint16_t len,
const uint8_t* value) {
if (len != kAseStatusCodecQosConfMinLen) {
LOG(ERROR) << "Wrong len of ASE characteristic (QOS conf header)";
return false;
}
STREAM_TO_UINT8(rsp.cig_id, value);
STREAM_TO_UINT8(rsp.cis_id, value);
STREAM_TO_UINT24(rsp.sdu_interval, value);
STREAM_TO_UINT8(rsp.framing, value);
STREAM_TO_UINT8(rsp.phy, value);
STREAM_TO_UINT16(rsp.max_sdu, value);
STREAM_TO_UINT8(rsp.retrans_nb, value);
STREAM_TO_UINT16(rsp.max_transport_latency, value);
STREAM_TO_UINT24(rsp.pres_delay, value);
LOG(INFO) << __func__ << ", Codec QoS Configured"
<< "\n\tCIG: " << loghex(rsp.cig_id)
<< "\n\tCIS: " << loghex(rsp.cis_id)
<< "\n\tSDU interval: " << loghex(rsp.sdu_interval)
<< "\n\tFraming: " << loghex(rsp.framing)
<< "\n\tPHY: " << loghex(rsp.phy)
<< "\n\tMax SDU: " << loghex(rsp.max_sdu)
<< "\n\tRetransmission number: " << loghex(rsp.retrans_nb)
<< "\n\tMax transport latency: "
<< loghex(rsp.max_transport_latency)
<< "\n\tPresentation delay: " << loghex(rsp.pres_delay);
return true;
}
bool ParseAseStatusTransientStateParams(struct ase_transient_state_params& rsp,
uint16_t len, const uint8_t* value) {
uint8_t metadata_len;
if (len < kAseStatusTransMinLen) {
LOG(ERROR) << "Wrong len of ASE characteristic (metadata)";
return false;
}
STREAM_TO_UINT8(rsp.cig_id, value);
STREAM_TO_UINT8(rsp.cis_id, value);
STREAM_TO_UINT8(metadata_len, value);
len -= kAseStatusTransMinLen;
if (len != metadata_len) {
LOG(ERROR) << "Wrong len of ASE characteristic (metadata)";
return false;
}
if (metadata_len > 0)
rsp.metadata = std::vector<uint8_t>(value, value + metadata_len);
LOG(INFO) << __func__ << ", Status enabling/streaming/disabling"
<< "\n\tCIG: " << loghex(rsp.cig_id)
<< "\n\tCIS: " << loghex(rsp.cis_id) << "\n\tMetadata: "
<< base::HexEncode(rsp.metadata.data(), rsp.metadata.size());
return true;
}
bool ParseAseCtpNotification(struct ctp_ntf& ntf, uint16_t len,
const uint8_t* value) {
uint8_t num_entries;
if (len < kCtpNtfMinLen) {
LOG(ERROR) << "Wrong len of ASE control point notification: " << (int)len;
return false;
}
STREAM_TO_UINT8(ntf.op, value);
STREAM_TO_UINT8(num_entries, value);
if (len != kCtpNtfMinLen + (num_entries * kCtpAseEntryMinLen)) {
LOG(ERROR) << "Wrong len of ASE control point notification (ASE IDs)";
return false;
}
for (int i = 0; i < num_entries; i++) {
struct ctp_ase_entry entry;
STREAM_TO_UINT8(entry.ase_id, value);
STREAM_TO_UINT8(entry.response_code, value);
STREAM_TO_UINT8(entry.reason, value);
ntf.entries.push_back(std::move(entry));
}
LOG(INFO) << __func__ << ", Control point notification"
<< "\n\tOpcode: " << ctp_opcode_map_string[ntf.op] << " ("
<< loghex(ntf.op) << ")"
<< "\n\tNum ASE IDs: " << (int)num_entries;
for (size_t i = 0; i < num_entries; i++)
LOG(INFO) << "\n\tASE ID[" << loghex(ntf.entries[i].ase_id)
<< "] response: "
<< ctp_response_code_map_string[ntf.entries[i].response_code]
<< " (" << loghex(ntf.entries[i].response_code) << ")"
<< " reason: " << ctp_reason_map_string[ntf.entries[i].reason]
<< " (" << loghex(ntf.entries[i].reason) << ")";
return true;
}
bool PrepareAseCtpCodecConfig(const std::vector<struct ctp_codec_conf>& confs,
std::vector<uint8_t>& value) {
if (confs.size() == 0) return false;
std::string conf_ents_str;
size_t msg_len = std::accumulate(
confs.begin(), confs.end(),
confs.size() * kCtpCodecConfMinLen + kAseNumSize + kCtpOpSize,
[&conf_ents_str](size_t cur_len, auto const& conf) {
auto ltv_map = conf.codec_config.GetAsLtvMap();
for (const auto& [type, value] : ltv_map.Values()) {
conf_ents_str +=
"\ttype: " + std::to_string(type) +
"\tlen: " + std::to_string(value.size()) +
"\tdata: " + base::HexEncode(value.data(), value.size()) + "\n";
};
return cur_len + ltv_map.RawPacketSize();
});
value.resize(msg_len);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeCodecConfiguration);
UINT8_TO_STREAM(msg, confs.size());
for (const struct ctp_codec_conf& conf : confs) {
UINT8_TO_STREAM(msg, conf.ase_id);
UINT8_TO_STREAM(msg, conf.target_latency);
UINT8_TO_STREAM(msg, conf.target_phy);
UINT8_TO_STREAM(msg, conf.codec_id.coding_format);
UINT16_TO_STREAM(msg, conf.codec_id.vendor_company_id);
UINT16_TO_STREAM(msg, conf.codec_id.vendor_codec_id);
auto ltv_map = conf.codec_config.GetAsLtvMap();
auto codec_spec_conf_len = ltv_map.RawPacketSize();
UINT8_TO_STREAM(msg, codec_spec_conf_len);
msg = ltv_map.RawPacket(msg);
LOG(INFO) << __func__ << ", Codec configuration"
<< "\n\tAse id: " << loghex(conf.ase_id)
<< "\n\tTarget latency: " << loghex(conf.target_latency)
<< "\n\tTarget PHY: " << loghex(conf.target_phy)
<< "\n\tCoding format: " << loghex(conf.codec_id.coding_format)
<< "\n\tVendor codec company ID: "
<< loghex(conf.codec_id.vendor_company_id)
<< "\n\tVendor codec ID: "
<< loghex(conf.codec_id.vendor_codec_id)
<< "\n\tCodec config len: "
<< static_cast<int>(codec_spec_conf_len)
<< "\n\tCodec spec conf: "
<< "\n"
<< conf_ents_str;
}
return true;
}
bool PrepareAseCtpConfigQos(const std::vector<struct ctp_qos_conf>& confs,
std::vector<uint8_t>& value) {
if (confs.size() == 0) return false;
value.resize(confs.size() * kCtpQosConfMinLen + kAseNumSize + kCtpOpSize);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeQosConfiguration);
UINT8_TO_STREAM(msg, confs.size());
for (const struct ctp_qos_conf& conf : confs) {
UINT8_TO_STREAM(msg, conf.ase_id);
UINT8_TO_STREAM(msg, conf.cig);
UINT8_TO_STREAM(msg, conf.cis);
UINT24_TO_STREAM(msg, conf.sdu_interval);
UINT8_TO_STREAM(msg, conf.framing);
UINT8_TO_STREAM(msg, conf.phy);
UINT16_TO_STREAM(msg, conf.max_sdu);
UINT8_TO_STREAM(msg, conf.retrans_nb);
UINT16_TO_STREAM(msg, conf.max_transport_latency);
UINT24_TO_STREAM(msg, conf.pres_delay);
LOG(INFO) << __func__ << ", QoS configuration"
<< "\n\tAse id: " << loghex(conf.ase_id)
<< "\n\tcig: " << loghex(conf.cig)
<< "\n\tCis: " << loghex(conf.cis)
<< "\n\tSDU interval: " << loghex(conf.sdu_interval)
<< "\n\tFraming: " << loghex(conf.framing)
<< "\n\tPhy: " << loghex(conf.phy)
<< "\n\tMax sdu size: " << loghex(conf.max_sdu)
<< "\n\tRetrans nb: " << loghex(conf.retrans_nb)
<< "\n\tMax Transport latency: "
<< loghex(conf.max_transport_latency)
<< "\n\tPres delay: " << loghex(conf.pres_delay);
}
return true;
}
bool PrepareAseCtpEnable(const std::vector<struct ctp_enable>& confs,
std::vector<uint8_t>& value) {
if (confs.size() == 0) return false;
uint16_t msg_len = confs.size() * kCtpEnableMinLen + kAseNumSize + kCtpOpSize;
std::for_each(confs.begin(), confs.end(),
[&msg_len](const struct ctp_enable& conf) {
msg_len += conf.metadata.size();
});
value.resize(msg_len);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeEnable);
UINT8_TO_STREAM(msg, confs.size());
for (const struct ctp_enable& conf : confs) {
UINT8_TO_STREAM(msg, conf.ase_id);
UINT8_TO_STREAM(msg, conf.metadata.size());
ARRAY_TO_STREAM(msg, conf.metadata.data(),
static_cast<int>(conf.metadata.size()));
LOG(INFO) << __func__ << ", Enable"
<< "\n\tAse id: " << loghex(conf.ase_id) << "\n\tMetadata: "
<< base::HexEncode(conf.metadata.data(), conf.metadata.size());
}
return true;
}
bool PrepareAseCtpAudioReceiverStartReady(const std::vector<uint8_t>& ase_ids,
std::vector<uint8_t>& value) {
if (ase_ids.size() == 0) return false;
value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeReceiverStartReady);
UINT8_TO_STREAM(msg, ase_ids.size());
for (const uint8_t& id : ase_ids) {
UINT8_TO_STREAM(msg, id);
LOG(INFO) << __func__ << ", ReceiverStartReady"
<< "\n\tAse id: " << loghex(id);
}
return true;
}
bool PrepareAseCtpDisable(const std::vector<uint8_t>& ase_ids,
std::vector<uint8_t>& value) {
if (ase_ids.size() == 0) return false;
value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeDisable);
UINT8_TO_STREAM(msg, ase_ids.size());
for (const uint8_t& id : ase_ids) {
UINT8_TO_STREAM(msg, id);
LOG(INFO) << __func__ << ", Disable"
<< "\n\tAse id: " << loghex(id);
}
return true;
}
bool PrepareAseCtpAudioReceiverStopReady(const std::vector<uint8_t>& ase_ids,
std::vector<uint8_t>& value) {
if (ase_ids.size() == 0) return false;
value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeReceiverStopReady);
UINT8_TO_STREAM(msg, ase_ids.size());
for (const uint8_t& ase_id : ase_ids) {
UINT8_TO_STREAM(msg, ase_id);
LOG(INFO) << __func__ << ", ReceiverStopReady"
<< "\n\tAse id: " << loghex(ase_id);
}
return true;
}
bool PrepareAseCtpUpdateMetadata(
const std::vector<struct ctp_update_metadata>& confs,
std::vector<uint8_t>& value) {
if (confs.size() == 0) return false;
uint16_t msg_len =
confs.size() * kCtpUpdateMetadataMinLen + kAseNumSize + kCtpOpSize;
std::for_each(confs.begin(), confs.end(),
[&msg_len](const struct ctp_update_metadata& conf) {
msg_len += conf.metadata.size();
});
value.resize(msg_len);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeUpdateMetadata);
UINT8_TO_STREAM(msg, confs.size());
for (const struct ctp_update_metadata& conf : confs) {
UINT8_TO_STREAM(msg, conf.ase_id);
UINT8_TO_STREAM(msg, conf.metadata.size());
ARRAY_TO_STREAM(msg, conf.metadata.data(),
static_cast<int>(conf.metadata.size()));
LOG(INFO) << __func__ << ", Update Metadata"
<< "\n\tAse id: " << loghex(conf.ase_id) << "\n\tMetadata: "
<< base::HexEncode(conf.metadata.data(), conf.metadata.size());
}
return true;
}
bool PrepareAseCtpRelease(const std::vector<uint8_t>& ase_ids,
std::vector<uint8_t>& value) {
if (ase_ids.size() == 0) return true;
value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize);
uint8_t* msg = value.data();
UINT8_TO_STREAM(msg, kCtpOpcodeRelease);
UINT8_TO_STREAM(msg, ase_ids.size());
for (const uint8_t& ase_id : ase_ids) {
UINT8_TO_STREAM(msg, ase_id);
LOG(INFO) << __func__ << ", Release"
<< "\n\tAse id: " << loghex(ase_id);
}
return true;
}
} // namespace ascs
namespace pacs {
bool ParsePac(std::vector<struct acs_ac_record>& pac_recs, uint16_t len,
const uint8_t* value) {
if (len < kAcsPacDiscoverRspMinLen) {
LOG(ERROR) << "Wrong len of PAC characteristic";
return false;
}
uint8_t pac_rec_nb;
STREAM_TO_UINT8(pac_rec_nb, value);
len -= kAcsPacDiscoverRspMinLen;
pac_recs.reserve(pac_rec_nb);
for (int i = 0; i < pac_rec_nb; i++) {
struct acs_ac_record rec;
uint8_t codec_spec_cap_len, metadata_len;
if (len < kAcsPacRecordMinLen) {
LOG(ERROR) << "Wrong len of PAC record";
pac_recs.clear();
return false;
}
STREAM_TO_UINT8(rec.codec_id.coding_format, value);
STREAM_TO_UINT16(rec.codec_id.vendor_company_id, value);
STREAM_TO_UINT16(rec.codec_id.vendor_codec_id, value);
STREAM_TO_UINT8(codec_spec_cap_len, value);
len -= kAcsPacRecordMinLen - kAcsPacMetadataLenLen;
if (len < codec_spec_cap_len + kAcsPacMetadataLenLen) {
LOG(ERROR) << "Wrong len of PAC record (codec specific capabilities)";
pac_recs.clear();
return false;
}
bool parsed;
rec.codec_spec_caps =
types::LeAudioLtvMap::Parse(value, codec_spec_cap_len, parsed);
if (!parsed) return false;
value += codec_spec_cap_len;
len -= codec_spec_cap_len;
STREAM_TO_UINT8(metadata_len, value);
len -= kAcsPacMetadataLenLen;
if (len < metadata_len) {
LOG(ERROR) << "Wrong len of PAC record (metadata)";
pac_recs.clear();
return false;
}
rec.metadata = std::vector<uint8_t>(value, value + metadata_len);
value += metadata_len;
len -= metadata_len;
pac_recs.push_back(std::move(rec));
}
return true;
}
bool ParseAudioLocations(types::AudioLocations& audio_locations, uint16_t len,
const uint8_t* value) {
if (len != kAudioLocationsRspMinLen) {
LOG(ERROR) << "Wrong len of Audio Location characteristic";
return false;
}
STREAM_TO_UINT32(audio_locations, value);
LOG(INFO) << "Audio locations: " << audio_locations.to_string();
return true;
}
bool ParseSupportedAudioContexts(struct acs_supported_audio_contexts& contexts,
uint16_t len, const uint8_t* value) {
if (len != kAseAudioSuppContRspMinLen) {
LOG(ERROR) << "Wrong len of Audio Supported Context characteristic";
return false;
}
STREAM_TO_UINT16(contexts.snk_supp_cont, value);
STREAM_TO_UINT16(contexts.src_supp_cont, value);
LOG(INFO) << "Supported Audio Contexts: "
<< "\n\tSupported Sink Contexts: "
<< contexts.snk_supp_cont.to_string()
<< "\n\tSupported Source Contexts: "
<< contexts.src_supp_cont.to_string();
return true;
}
bool ParseAvailableAudioContexts(struct acs_available_audio_contexts& contexts,
uint16_t len, const uint8_t* value) {
if (len != kAseAudioAvailRspMinLen) {
LOG(ERROR) << "Wrong len of Audio Availability characteristic";
return false;
}
STREAM_TO_UINT16(contexts.snk_avail_cont, value);
STREAM_TO_UINT16(contexts.src_avail_cont, value);
LOG(INFO) << "Available Audio Contexts: "
<< "\n\tAvailable Sink Contexts: "
<< contexts.snk_avail_cont.to_string()
<< "\n\tAvailable Source Contexts: "
<< contexts.src_avail_cont.to_string();
return true;
}
} // namespace pacs
} // namespace client_parser
} // namespace le_audio