8#pragma comment(lib, "avrt.lib")
9#pragma comment(lib, "ole32.lib")
24 void check_hr(HRESULT hr,
const char* msg, std::source_location loc = std::source_location::current())
27 error(C, X, loc,
"{} (HRESULT 0x{:08X})", msg,
static_cast<unsigned>(hr));
31 std::string wstring_to_utf8(
const std::wstring& ws)
35 int sz = WideCharToMultiByte(CP_UTF8, 0, ws.data(),
static_cast<int>(ws.size()),
36 nullptr, 0,
nullptr,
nullptr);
37 std::string out(sz,
'\0');
38 WideCharToMultiByte(CP_UTF8, 0, ws.data(),
static_cast<int>(ws.size()),
39 out.data(), sz,
nullptr,
nullptr);
43 std::wstring get_device_friendly_name(IMMDevice* dev)
45 IPropertyStore* props =
nullptr;
46 if (FAILED(dev->OpenPropertyStore(STGM_READ, &props)))
52 static const PROPERTYKEY k_friendly_name = {
53 { 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } }, 14
58 std::wstring name =
L"Unknown";
60 if (SUCCEEDED(props->GetValue(k_friendly_name, &pv))
61 && pv.vt == VT_LPWSTR && pv.pwszVal) {
64 PropVariantClear(&pv);
75WasapiBackend::WasapiBackend()
77 check_hr(CoInitializeEx(
nullptr, COINIT_MULTITHREADED),
78 "WasapiBackend: CoInitializeEx failed");
80 check_hr(CoCreateInstance(
81 __uuidof(MMDeviceEnumerator),
nullptr, CLSCTX_ALL,
82 __uuidof(IMMDeviceEnumerator),
83 reinterpret_cast<void**
>(&m_enumerator)),
84 "WasapiBackend: CoCreateInstance(MMDeviceEnumerator) failed");
86 MF_INFO(C, X,
"WasapiBackend initialised (Windows WASAPI shared mode)");
89WasapiBackend::~WasapiBackend()
94std::unique_ptr<AudioDevice> WasapiBackend::create_device_manager()
96 return std::make_unique<WasapiDevice>(m_enumerator);
99std::unique_ptr<AudioStream> WasapiBackend::create_stream(
100 unsigned int output_device_id,
101 unsigned int input_device_id,
102 GlobalStreamInfo& stream_info,
105 auto dev_mgr = WasapiDevice(m_enumerator);
107 IMMDevice* out_dev = dev_mgr.resolve_device(output_device_id, eRender);
109 error(C, X, std::source_location::current(),
110 "WasapiBackend: failed to resolve output device id {}", output_device_id);
112 IMMDevice* in_dev =
nullptr;
113 if (stream_info.input.enabled && stream_info.input.channels > 0)
114 in_dev = dev_mgr.resolve_device(input_device_id, eCapture);
116 return std::make_unique<WasapiStream>(out_dev, in_dev, stream_info, user_data);
119std::string WasapiBackend::get_version_string()
const
121 return "WASAPI (Windows native shared mode)";
124void WasapiBackend::cleanup()
127 m_enumerator->Release();
128 m_enumerator =
nullptr;
137WasapiDevice::WasapiDevice(IMMDeviceEnumerator* enumerator)
138 : m_enumerator(enumerator)
140 enumerate(enumerator, eRender, m_outputs, m_default_output);
141 enumerate(enumerator, eCapture, m_inputs, m_default_input);
144WasapiDevice::~WasapiDevice() =
default;
146void WasapiDevice::enumerate(
147 IMMDeviceEnumerator* enumerator,
149 std::vector<EndpointEntry>& out,
150 unsigned int& default_idx)
154 IMMDevice* def_dev =
nullptr;
156 if (SUCCEEDED(enumerator->GetDefaultAudioEndpoint(flow, eConsole, &def_dev))) {
158 if (SUCCEEDED(def_dev->GetId(&
id))) {
165 IMMDeviceCollection* col =
nullptr;
166 if (FAILED(enumerator->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &col)))
170 col->GetCount(&
count);
172 for (UINT i = 0; i <
count; ++i) {
173 IMMDevice* dev =
nullptr;
174 if (FAILED(col->Item(i, &dev)))
177 LPWSTR ep_id =
nullptr;
178 std::wstring ep_id_str;
179 if (SUCCEEDED(dev->GetId(&ep_id))) {
181 CoTaskMemFree(ep_id);
184 IAudioClient* client =
nullptr;
185 uint32_t preferred_rate = 48000;
186 uint32_t channels = (flow == eRender) ? 2u : 1u;
188 if (SUCCEEDED(dev->Activate(__uuidof(IAudioClient), CLSCTX_ALL,
nullptr,
189 reinterpret_cast<void**
>(&client)))) {
190 WAVEFORMATEX* mix_fmt =
nullptr;
191 if (SUCCEEDED(client->GetMixFormat(&mix_fmt)) && mix_fmt) {
192 preferred_rate = mix_fmt->nSamplesPerSec;
193 channels = mix_fmt->nChannels;
194 CoTaskMemFree(mix_fmt);
200 info.name = wstring_to_utf8(get_device_friendly_name(dev));
201 info.preferred_sample_rate = preferred_rate;
202 info.output_channels = (flow == eRender) ? channels : 0;
203 info.input_channels = (flow == eCapture) ? channels : 0;
204 info.is_default_output = (flow == eRender && ep_id_str == def_id);
205 info.is_default_input = (flow == eCapture && ep_id_str == def_id);
207 if (info.is_default_output || info.is_default_input)
208 default_idx =
static_cast<unsigned int>(out.size() + 1);
210 out.push_back({ info, ep_id_str });
217std::vector<DeviceInfo> WasapiDevice::get_output_devices()
const
219 std::vector<DeviceInfo> result;
220 result.reserve(m_outputs.size());
221 for (
const auto& e : m_outputs)
222 result.push_back(e.info);
226std::vector<DeviceInfo> WasapiDevice::get_input_devices()
const
228 std::vector<DeviceInfo> result;
229 result.reserve(m_inputs.size());
230 for (
const auto& e : m_inputs)
231 result.push_back(e.info);
235unsigned int WasapiDevice::get_default_output_device()
const {
return m_default_output; }
236unsigned int WasapiDevice::get_default_input_device()
const {
return m_default_input; }
238IMMDevice* WasapiDevice::resolve_device(
unsigned int id, EDataFlow flow)
const
240 const auto& list = (flow == eRender) ? m_outputs : m_inputs;
243 IMMDevice* dev =
nullptr;
244 if (SUCCEEDED(m_enumerator->GetDefaultAudioEndpoint(flow, eConsole, &dev)))
249 const unsigned int idx =
id - 1;
250 if (idx >= list.size())
253 IMMDevice* dev =
nullptr;
254 m_enumerator->GetDevice(list[idx].endpoint_id.c_str(), &dev);
262WasapiStream::WasapiStream(
263 IMMDevice* output_device,
264 IMMDevice* input_device,
265 GlobalStreamInfo& stream_info,
267 : m_output_device(output_device)
268 , m_input_device(input_device)
269 , m_stream_info(stream_info)
270 , m_user_data(user_data)
274WasapiStream::~WasapiStream()
276 if (is_running() || is_open())
280bool WasapiStream::negotiate_format(
281 IAudioClient* client,
283 uint32_t sample_rate,
284 WAVEFORMATEX** out_fmt)
286 auto* wfx =
static_cast<WAVEFORMATEXTENSIBLE*
>(
287 CoTaskMemAlloc(
sizeof(WAVEFORMATEXTENSIBLE)));
291 std::memset(wfx, 0,
sizeof(WAVEFORMATEXTENSIBLE));
292 wfx->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
293 wfx->Format.nChannels =
static_cast<WORD
>(channels);
294 wfx->Format.nSamplesPerSec = sample_rate;
295 wfx->Format.wBitsPerSample = 32;
296 wfx->Format.nBlockAlign =
static_cast<WORD
>(channels *
sizeof(float));
297 wfx->Format.nAvgBytesPerSec = wfx->Format.nBlockAlign * sample_rate;
298 wfx->Format.cbSize =
sizeof(WAVEFORMATEXTENSIBLE) -
sizeof(WAVEFORMATEX);
299 wfx->Samples.wValidBitsPerSample = 32;
300 wfx->dwChannelMask = (channels == 2)
301 ? (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT)
302 : KSAUDIO_SPEAKER_DIRECTOUT;
303 wfx->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
305 WAVEFORMATEX* closest =
nullptr;
306 HRESULT hr = client->IsFormatSupported(
307 AUDCLNT_SHAREMODE_SHARED,
308 reinterpret_cast<WAVEFORMATEX*
>(wfx),
312 *out_fmt =
reinterpret_cast<WAVEFORMATEX*
>(wfx);
314 CoTaskMemFree(closest);
320 if (hr == S_FALSE && closest) {
322 "WASAPI shared mode: negotiated mix format ({} Hz, {} ch); "
323 "engine converts float64<->float32 on the render path.",
324 closest->nSamplesPerSec, closest->nChannels);
330 CoTaskMemFree(closest);
332 WAVEFORMATEX*
mix =
nullptr;
333 if (SUCCEEDED(client->GetMixFormat(&mix))) {
335 "WASAPI shared mode: using device mix format ({} Hz, {} ch).",
336 mix->nSamplesPerSec,
mix->nChannels);
344void WasapiStream::open()
346 if (m_is_open.load())
349 m_stop_event = CreateEventW(
nullptr, TRUE, FALSE,
nullptr);
351 error(C, X, std::source_location::current(),
"WasapiStream: CreateEvent (stop) failed");
354 check_hr(m_output_device->Activate(
355 __uuidof(IAudioClient), CLSCTX_ALL,
nullptr,
356 reinterpret_cast<void**
>(&m_render_client)),
357 "WasapiStream: Activate render IAudioClient failed");
359 WAVEFORMATEX* fmt =
nullptr;
360 if (!negotiate_format(m_render_client,
361 m_stream_info.output.channels,
362 m_stream_info.sample_rate, &fmt))
364 error(C, X, std::source_location::current(),
365 "WasapiStream: render format negotiation failed");
367 m_negotiated_output_rate = fmt->nSamplesPerSec;
368 m_negotiated_output_channels = fmt->nChannels;
370 if (m_negotiated_output_rate != m_stream_info.sample_rate) {
371 MF_WARN(C, X,
"WASAPI render: negotiated {} Hz (requested {})",
372 m_negotiated_output_rate, m_stream_info.sample_rate);
373 m_stream_info.sample_rate = m_negotiated_output_rate;
376 if (m_negotiated_output_channels != m_stream_info.output.channels) {
377 MF_WARN(C, X,
"WASAPI render: negotiated {} channels (requested {})",
378 m_negotiated_output_channels, m_stream_info.output.channels);
379 m_stream_info.output.channels = m_negotiated_output_channels;
382 const REFERENCE_TIME period =
static_cast<REFERENCE_TIME
>(
383 10000.0 * 1000.0 * m_stream_info.buffer_size / m_stream_info.sample_rate);
385 check_hr(m_render_client->Initialize(
386 AUDCLNT_SHAREMODE_SHARED,
387 AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
388 period, 0, fmt,
nullptr),
389 "WasapiStream: IAudioClient::Initialize (render) failed");
391 m_render_event = CreateEventW(
nullptr, FALSE, FALSE,
nullptr);
393 error(C, X, std::source_location::current(),
394 "WasapiStream: CreateEvent (render) failed");
396 check_hr(m_render_client->SetEventHandle(m_render_event),
397 "WasapiStream: SetEventHandle (render) failed");
399 check_hr(m_render_client->GetBufferSize(&m_render_buffer_frames),
400 "WasapiStream: GetBufferSize (render) failed");
402 if (m_render_buffer_frames != m_stream_info.buffer_size)
403 MF_INFO(C, X,
"WASAPI shared mode buffer: {} frames (requested {})",
404 m_render_buffer_frames, m_stream_info.buffer_size);
406 check_hr(m_render_client->GetService(
407 __uuidof(IAudioRenderClient),
408 reinterpret_cast<void**
>(&m_render_sink)),
409 "WasapiStream: GetService(IAudioRenderClient) failed");
413 m_ring_frames = std::max(m_stream_info.buffer_size, m_render_buffer_frames) * 4;
414 m_ring_write_pos = 0;
416 m_render_ring.assign(
417 static_cast<size_t>(m_ring_frames) * m_stream_info.output.channels, 0.0);
419 m_output_staging.resize(
420 static_cast<size_t>(m_stream_info.buffer_size) * m_stream_info.output.channels);
421 m_render_staging.resize(m_output_staging.size());
424 if (m_input_device) {
425 check_hr(m_input_device->Activate(
426 __uuidof(IAudioClient), CLSCTX_ALL,
nullptr,
427 reinterpret_cast<void**
>(&m_capture_client)),
428 "WasapiStream: Activate capture IAudioClient failed");
430 WAVEFORMATEX* fmt =
nullptr;
431 if (!negotiate_format(m_capture_client,
432 m_stream_info.input.channels,
433 m_stream_info.sample_rate, &fmt)) {
434 MF_WARN(C, X,
"WasapiStream: capture format negotiation failed; input disabled");
435 m_capture_client->Release();
436 m_capture_client =
nullptr;
438 m_negotiated_input_rate = fmt->nSamplesPerSec;
439 m_negotiated_input_channels = fmt->nChannels;
441 const REFERENCE_TIME period =
static_cast<REFERENCE_TIME
>(
442 10000.0 * 1000.0 * m_stream_info.buffer_size / m_stream_info.sample_rate);
444 if (FAILED(m_capture_client->Initialize(
445 AUDCLNT_SHAREMODE_SHARED,
446 AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
447 period, 0, fmt,
nullptr))) {
448 MF_WARN(C, X,
"WasapiStream: IAudioClient::Initialize (capture) failed; input disabled");
449 m_capture_client->Release();
450 m_capture_client =
nullptr;
452 m_capture_event = CreateEventW(
nullptr, FALSE, FALSE,
nullptr);
453 m_capture_client->SetEventHandle(m_capture_event);
456 m_capture_client->GetBufferSize(&cap_frames);
457 m_capture_buffer_frames = cap_frames;
459 m_cap_ring_frames = std::max(m_stream_info.buffer_size, m_capture_buffer_frames) * 4;
460 m_cap_ring_write_pos = 0;
461 m_cap_ring_read_pos = 0;
462 m_capture_ring.assign(
463 static_cast<size_t>(m_cap_ring_frames) * m_stream_info.input.channels, 0.0);
465 m_capture_client->GetService(
466 __uuidof(IAudioCaptureClient),
467 reinterpret_cast<void**
>(&m_capture_src));
469 m_capture_staging.resize(
470 static_cast<size_t>(m_capture_buffer_frames) * m_stream_info.input.channels);
471 m_input_staging.resize(m_capture_staging.size());
477 m_is_open.store(
true, std::memory_order_release);
478 MF_INFO(C, X,
"WasapiStream opened ({} Hz, {} out-ch, engine buffer {} frames, hw buffer {} frames)",
479 m_stream_info.sample_rate, m_stream_info.output.channels,
480 m_stream_info.buffer_size, m_render_buffer_frames);
483void WasapiStream::start()
485 if (!m_is_open.load())
486 error(C, X, std::source_location::current(),
487 "WasapiStream::start() called before open()");
488 if (m_is_running.load())
491 ResetEvent(m_stop_event);
493 if (m_capture_client)
494 m_capture_client->Start();
495 m_render_client->Start();
497 m_render_thread = CreateThread(
498 nullptr, 0, render_thread_proc,
this, 0,
nullptr);
499 if (!m_render_thread)
500 error(C, X, std::source_location::current(),
501 "WasapiStream: CreateThread failed");
503 m_is_running.store(
true, std::memory_order_release);
506void WasapiStream::stop()
508 if (!m_is_running.load())
511 SetEvent(m_stop_event);
513 if (m_render_thread) {
514 WaitForSingleObject(m_render_thread, 2000);
515 CloseHandle(m_render_thread);
516 m_render_thread =
nullptr;
519 m_render_client->Stop();
520 if (m_capture_client)
521 m_capture_client->Stop();
523 m_is_running.store(
false, std::memory_order_release);
526void WasapiStream::pause()
528 if (!m_is_running.load() || m_is_paused.load())
531 SetEvent(m_stop_event);
532 if (m_render_thread) {
533 WaitForSingleObject(m_render_thread, 2000);
534 CloseHandle(m_render_thread);
535 m_render_thread =
nullptr;
538 m_render_client->Stop();
539 if (m_capture_client)
540 m_capture_client->Stop();
542 m_is_paused.store(
true, std::memory_order_release);
543 m_is_running.store(
false, std::memory_order_release);
546void WasapiStream::resume()
548 if (!m_is_paused.load())
551 ResetEvent(m_stop_event);
553 if (m_capture_client)
554 m_capture_client->Start();
555 m_render_client->Start();
557 m_render_thread = CreateThread(
558 nullptr, 0, render_thread_proc,
this, 0,
nullptr);
559 if (!m_render_thread)
560 error(C, X, std::source_location::current(),
561 "WasapiStream: CreateThread failed on resume");
563 m_is_paused.store(
false, std::memory_order_release);
564 m_is_running.store(
true, std::memory_order_release);
567void WasapiStream::close()
569 if (!m_is_open.load())
572 if (m_is_running.load() || m_is_paused.load())
576 m_render_sink->Release();
577 m_render_sink =
nullptr;
579 if (m_render_client) {
580 m_render_client->Release();
581 m_render_client =
nullptr;
584 m_capture_src->Release();
585 m_capture_src =
nullptr;
587 if (m_capture_client) {
588 m_capture_client->Release();
589 m_capture_client =
nullptr;
592 if (m_output_device) {
593 m_output_device->Release();
594 m_output_device =
nullptr;
596 if (m_input_device) {
597 m_input_device->Release();
598 m_input_device =
nullptr;
601 if (m_render_event) {
602 CloseHandle(m_render_event);
603 m_render_event =
nullptr;
605 if (m_capture_event) {
606 CloseHandle(m_capture_event);
607 m_capture_event =
nullptr;
610 CloseHandle(m_stop_event);
611 m_stop_event =
nullptr;
614 m_cap_ring_write_pos = 0;
615 m_cap_ring_read_pos = 0;
616 m_ring_write_pos = 0;
619 m_is_open.store(
false, std::memory_order_release);
622bool WasapiStream::is_running()
const {
return m_is_running.load(std::memory_order_acquire); }
623bool WasapiStream::is_open()
const {
return m_is_open.load(std::memory_order_acquire); }
625void WasapiStream::set_process_callback(
626 std::function<
int(
void*,
void*,
unsigned int)> cb)
628 m_process_callback = std::move(cb);
631DWORD WINAPI WasapiStream::render_thread_proc(LPVOID param)
633 auto* self =
static_cast<WasapiStream*
>(param);
636 HANDLE task = AvSetMmThreadCharacteristicsW(L
"Pro Audio", &task_idx);
641 AvRevertMmThreadCharacteristics(task);
646void WasapiStream::render_loop()
648 const uint32_t out_ch = m_negotiated_output_channels;
649 const uint32_t in_ch_wire = m_negotiated_input_channels;
650 const uint32_t in_ch_engine = m_stream_info.input.channels;
651 const uint32_t hw_frames = m_render_buffer_frames;
652 const uint32_t eng_frames = m_stream_info.buffer_size;
653 const uint32_t ring_frames = m_ring_frames;
654 const uint32_t ring_ch = m_stream_info.output.channels;
655 const uint32_t cap_ring_frames = m_cap_ring_frames;
657 HANDLE wait_handles[2] = { m_render_event, m_stop_event };
660 DWORD wait = WaitForMultipleObjects(2, wait_handles, FALSE, 2000);
661 if (wait == WAIT_OBJECT_0 + 1 || wait == WAIT_FAILED || wait == WAIT_TIMEOUT)
664 if (m_capture_src && in_ch_wire > 0) {
665 BYTE* data =
nullptr;
669 if (SUCCEEDED(m_capture_src->GetBuffer(&data, &cap_frames, &flags,
nullptr,
nullptr))) {
670 if (data && !(flags & AUDCLNT_BUFFERFLAGS_SILENT)) {
671 const auto* src =
reinterpret_cast<const float*
>(data);
672 const uint32_t copy_ch = std::min(in_ch_wire, in_ch_engine);
673 for (uint32_t f = 0; f < cap_frames; ++f) {
674 const uint32_t dst = m_cap_ring_write_pos * in_ch_engine;
675 for (uint32_t c = 0; c < copy_ch; ++c)
676 m_capture_ring[dst + c] =
static_cast<double>(src[f * in_ch_wire + c]);
677 for (uint32_t c = copy_ch; c < in_ch_engine; ++c)
678 m_capture_ring[dst + c] = 0.0;
679 m_cap_ring_write_pos = (m_cap_ring_write_pos + 1) % cap_ring_frames;
682 m_capture_src->ReleaseBuffer(cap_frames);
686 auto cap_ring_used = [&]() -> uint32_t {
687 return (m_cap_ring_write_pos >= m_cap_ring_read_pos)
688 ? m_cap_ring_write_pos - m_cap_ring_read_pos
689 : cap_ring_frames - m_cap_ring_read_pos + m_cap_ring_write_pos;
692 auto ring_used = [&]() -> uint32_t {
693 return (m_ring_write_pos >= m_ring_read_pos)
694 ? m_ring_write_pos - m_ring_read_pos
695 : ring_frames - m_ring_read_pos + m_ring_write_pos;
698 while (ring_used() < hw_frames && m_process_callback) {
699 const size_t n =
static_cast<size_t>(eng_frames) * ring_ch;
700 if (n > m_output_staging.size())
701 m_output_staging.resize(n);
703 std::fill(m_output_staging.begin(), m_output_staging.begin() + n, 0.0);
705 double* in_ptr =
nullptr;
706 if (cap_ring_used() >= eng_frames) {
707 const size_t in_n =
static_cast<size_t>(eng_frames) * in_ch_engine;
708 if (in_n > m_input_staging.size())
709 m_input_staging.resize(in_n);
711 for (uint32_t f = 0; f < eng_frames; ++f) {
712 const uint32_t src = m_cap_ring_read_pos * in_ch_engine;
713 const uint32_t dst = f * in_ch_engine;
714 for (uint32_t c = 0; c < in_ch_engine; ++c)
715 m_input_staging[dst + c] = m_capture_ring[src + c];
716 m_cap_ring_read_pos = (m_cap_ring_read_pos + 1) % cap_ring_frames;
718 in_ptr = m_input_staging.data();
721 m_process_callback(m_output_staging.data(), in_ptr, eng_frames);
723 for (uint32_t i = 0; i < eng_frames; ++i) {
724 const uint32_t dst = m_ring_write_pos * ring_ch;
725 const uint32_t src = i * ring_ch;
726 for (uint32_t c = 0; c < ring_ch; ++c)
727 m_render_ring[dst + c] = m_output_staging[src + c];
728 m_ring_write_pos = (m_ring_write_pos + 1) % ring_frames;
733 if (FAILED(m_render_client->GetCurrentPadding(&padding)))
736 const UINT32 available = hw_frames - padding;
740 const UINT32 to_write = std::min(available, ring_used());
745 if (FAILED(m_render_sink->GetBuffer(to_write, &buf)) || !buf)
748 auto* dst =
reinterpret_cast<float*
>(buf);
749 for (uint32_t i = 0; i < to_write; ++i) {
750 const uint32_t src = m_ring_read_pos * ring_ch;
751 for (uint32_t c = 0; c < out_ch && c < ring_ch; ++c)
752 dst[i * out_ch + c] =
static_cast<float>(m_render_ring[src + c]);
753 m_ring_read_pos = (m_ring_read_pos + 1) % ring_frames;
756 m_render_sink->ReleaseBuffer(to_write, 0);
#define MF_INFO(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
@ AudioBackend
Audio processing backend (Pipewire, wasapi, coreaudio)
void error(Component component, Context context, std::source_location location, std::string_view message)
Log an error message and optionally throw an exception.
@ Core
Core engine, backend, subsystems.
void stop()
Stop all Portal::Graphics operations.
std::vector< double > mix(const std::vector< std::vector< double > > &streams)
Mix multiple data streams with equal weighting.