/*************************************************************************** * Copyright (C) 2006-2021 by Ilya Kotov * * forkotov02@ya.ru * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include #include #include #include #include #include "outputpipewire.h" OutputPipeWire *OutputPipeWire::instance = nullptr; VolumePipeWire *OutputPipeWire::volumeControl = nullptr; OutputPipeWire::OutputPipeWire(): Output() { m_pw_channels = { { Qmmp::CHAN_NULL, SPA_AUDIO_CHANNEL_NA }, { Qmmp::CHAN_FRONT_CENTER, SPA_AUDIO_CHANNEL_MONO }, { Qmmp::CHAN_FRONT_LEFT, SPA_AUDIO_CHANNEL_FL }, { Qmmp::CHAN_FRONT_RIGHT, SPA_AUDIO_CHANNEL_FR }, { Qmmp::CHAN_REAR_LEFT, SPA_AUDIO_CHANNEL_RL }, { Qmmp::CHAN_REAR_RIGHT, SPA_AUDIO_CHANNEL_RR }, { Qmmp::CHAN_FRONT_CENTER, SPA_AUDIO_CHANNEL_FC }, { Qmmp::CHAN_LFE, SPA_AUDIO_CHANNEL_LFE }, { Qmmp::CHAN_SIDE_LEFT, SPA_AUDIO_CHANNEL_SL }, { Qmmp::CHAN_SIDE_RIGHT, SPA_AUDIO_CHANNEL_SR }, { Qmmp::CHAN_REAR_CENTER, SPA_AUDIO_CHANNEL_RC } }; pw_init(nullptr, nullptr); instance = this; } OutputPipeWire::~OutputPipeWire() { uninitialize(); pw_deinit(); instance = nullptr; } void OutputPipeWire::onStateChanged (void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { OutputPipeWire *o = static_cast(data); if (o->m_ignoreStateChange) return; switch (state) { case PW_STREAM_STATE_UNCONNECTED: pw_thread_loop_signal(o->m_loop, false); break; case PW_STREAM_STATE_PAUSED: o->m_streamPaused = true; qDebug("paused"); pw_thread_loop_signal(o->m_loop, false); break; case PW_STREAM_STATE_STREAMING: o->m_streamPaused = false; pw_thread_loop_signal(o->m_loop, false); if(volumeControl) instance->setMuted(volumeControl->isMuted()); break; default: break; } //pw_thread_loop_signal(o->m_loop, false); } bool OutputPipeWire::initialize(quint32 freq, ChannelMap map, Qmmp::AudioFormat format) { static const pw_core_events coreEvents = { .version = PW_VERSION_CORE_EVENTS, .done = OutputPipeWire::onCoreEventDone }; static const pw_registry_events registryEvents = { .version = PW_VERSION_REGISTRY_EVENTS, .global = OutputPipeWire::onRegistryEventGlobal }; static const struct pw_stream_events streamEvents = { .version = PW_VERSION_STREAM_EVENTS, .state_changed = OutputPipeWire::onStateChanged, .process = OutputPipeWire::onProcess, .drained = OutputPipeWire::onDrained }; if(!(m_loop = pw_thread_loop_new("pipewire-main-loop", nullptr))) { qWarning("OutputPipeWire: unable to create main loop"); return false; } if (!(m_context = pw_context_new(pw_thread_loop_get_loop(m_loop), nullptr, 0))) { qWarning("OutputPipeWire: unable to create context"); return false; } if(!(m_core = pw_context_connect(m_context, nullptr, 0))) { qWarning("OutputPipeWire: unable to connect context"); return false; } pw_core_add_listener(m_core, &m_coreListener, &coreEvents, this); if(!(m_registry = pw_core_get_registry(m_core, PW_VERSION_REGISTRY, 0))) { qWarning("OutputPipeWire: unable to get registry interface"); return false; } pw_registry_add_listener(m_registry, &m_registryListener, ®istryEvents, this); m_coreInitSeq = pw_core_sync(m_core, PW_ID_CORE, m_coreInitSeq); if (pw_thread_loop_start(m_loop) != 0) { //m_err = true; return false; } pw_thread_loop_lock(m_loop); while (!m_initDone) { if (pw_thread_loop_timed_wait(m_loop, 2) != 0) break; } pw_thread_loop_unlock(m_loop); if(!m_initDone || !m_hasSinks) return false; m_stride = AudioParameters::sampleSize(format) * map.count(); m_nFrames = qBound(64, qCeil((2048.0 / 48000.0) * freq), 8192); m_bufferSize = m_nFrames * m_stride; m_writeBufferPos = 0; m_buffer = new unsigned char[m_bufferSize]; auto props = pw_properties_new( PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback", PW_KEY_MEDIA_ROLE, "Music", PW_KEY_MEDIA_ICON_NAME, "qmmp", nullptr ); pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", m_nFrames, freq); pw_thread_loop_lock(m_loop); if (!(m_stream = pw_stream_new(m_core, "Playback", props))) { qWarning("OutputPipeWire: unable to create stream"); pw_thread_loop_unlock(m_loop); return false; } m_streamListener = {}; pw_stream_add_listener(m_stream, &m_streamListener, &streamEvents, this); uint8_t buffer[1024]; spa_pod_builder b = { .data = buffer, .size = sizeof(buffer) }; spa_audio_info_raw info = { .format = SPA_AUDIO_FORMAT_F32, .flags = SPA_AUDIO_FLAG_NONE, .rate = freq, .channels = map.count() }; switch (format) { case Qmmp::PCM_S8: info.format = SPA_AUDIO_FORMAT_S8; break; case Qmmp::PCM_S16LE: info.format = SPA_AUDIO_FORMAT_S16_LE; break; case Qmmp::PCM_S24LE: info.format = SPA_AUDIO_FORMAT_S24_32_LE; break; case Qmmp::PCM_S32LE: info.format = SPA_AUDIO_FORMAT_S32_LE; break; case Qmmp::PCM_FLOAT: info.format = SPA_AUDIO_FORMAT_F32_LE; break; default: info.format = SPA_AUDIO_FORMAT_S16_LE; } for(int i = 0; i < map.count(); i++) info.position[i] = m_pw_channels[map.value(i)]; const spa_pod *params[2]; params[0] = spa_format_audio_raw_build( &b, SPA_PARAM_EnumFormat, &info ); /*params[1] = static_cast(spa_pod_builder_add_object( &b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_size, m_bufferSize) );*/ qDebug() << pw_stream_connect(m_stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), params, 1); Output::configure(freq, map, format); pw_thread_loop_unlock(m_loop); qDebug("OutputPipeWire: ready"); return true; } qint64 OutputPipeWire::latency() { /*pa_usec_t usec; int negative; int r = pa_stream_get_latency(m_stream, &usec, &negative); return (r == PA_OK && negative == 0) ? (usec / 1000) : 0;*/ return 0; } qint64 OutputPipeWire::writeAudio(unsigned char *data, qint64 maxSize) { //qDebug() << Q_FUNC_INFO; pw_thread_loop_lock(m_loop); if(m_buffer_at == m_bufferSize) { if (pw_thread_loop_timed_wait(m_loop, 1) != 0) { pw_thread_loop_unlock(m_loop); return 0; } } qint64 size = qMin(qint64(m_bufferSize - m_buffer_at), maxSize); memcpy(m_buffer + m_buffer_at, data, size); m_buffer_at += size; pw_thread_loop_unlock(m_loop); return size; } void OutputPipeWire::drain() { pw_thread_loop_lock(m_loop); if(m_buffer_at > 0) pw_thread_loop_timed_wait(m_loop, 2); pw_stream_flush(m_stream, true); pw_thread_loop_timed_wait(m_loop, 2); pw_thread_loop_unlock(m_loop); } void OutputPipeWire::reset() { pw_thread_loop_lock(m_loop); m_buffer_at = 0; pw_thread_loop_unlock(m_loop); pw_stream_flush(m_stream, false); } void OutputPipeWire::suspend() { pw_thread_loop_lock(m_loop); pw_stream_set_active(m_stream, false); pw_thread_loop_unlock(m_loop); } void OutputPipeWire::resume() { pw_thread_loop_lock(m_loop); pw_stream_set_active(m_stream, true); pw_thread_loop_unlock(m_loop); } void OutputPipeWire::setMuted(bool mute) { setVolume(mute ? VolumeSettings() : volumeControl->volume()); } void OutputPipeWire::setVolume(const VolumeSettings &v) { qDebug() << Q_FUNC_INFO << v.left << v.right; pw_thread_loop_lock(m_loop); float* values = new float[channels()]; if(channels() == 2) { values[0] = float(v.left) / 100.0; values[1] = float(v.right) / 100.0; } else { for(int i = 0; i < channels(); ++i) values[i] = float(qMax(v.left, v.right)) / 100.0; } pw_stream_set_control(m_stream, SPA_PROP_channelVolumes, channels(), values, 0); delete[] values; pw_thread_loop_unlock(m_loop); } void OutputPipeWire::uninitialize() { if(m_stream) { pw_thread_loop_lock(m_loop); m_ignoreStateChange = true; pw_stream_disconnect(m_stream); pw_stream_destroy(m_stream); m_ignoreStateChange = false; m_stream = nullptr; pw_thread_loop_unlock(m_loop); } if(m_loop) pw_thread_loop_stop(m_loop); if(m_registry) { pw_proxy_destroy(reinterpret_cast(m_registry)); m_registry = nullptr; } if(m_core) { pw_core_disconnect(m_core); m_core = nullptr; } if(m_context) { pw_context_destroy(m_context); m_context = nullptr; } if(m_loop) { pw_thread_loop_destroy(m_loop); m_loop = nullptr; } if(m_buffer) { delete [] m_buffer; m_buffer = nullptr; } } //callbacks void OutputPipeWire::onProcess(void *data) { OutputPipeWire *o = static_cast(data); if(!o->m_buffer_at) { pw_thread_loop_signal(o->m_loop, false); return; } struct pw_buffer *b = pw_stream_dequeue_buffer(o->m_stream); struct spa_buffer *buf = b->buffer; uint32_t size = qMin(buf->datas[0].maxsize, uint32_t(o->m_buffer_at)); memcpy(buf->datas[0].data, o->m_buffer, size); o->m_buffer_at -= size; memmove(o->m_buffer, o->m_buffer + size, o->m_buffer_at); b->buffer->datas[0].chunk->offset = 0; b->buffer->datas[0].chunk->size = o->m_bufferSize; b->buffer->datas[0].chunk->stride = o->m_stride; pw_stream_queue_buffer(o->m_stream, b); pw_thread_loop_signal(o->m_loop, false); } void OutputPipeWire::onDrained(void *data) { OutputPipeWire *o = static_cast(data); pw_thread_loop_signal(o->m_loop, false); qDebug("drained"); } void OutputPipeWire::onCoreEventDone(void *data, uint32_t id, int seq) { OutputPipeWire *o = static_cast(data); if (id == PW_ID_CORE && seq == o->m_coreInitSeq) { spa_hook_remove(&o->m_registryListener); spa_hook_remove(&o->m_coreListener); o->m_initDone = true; pw_thread_loop_signal(o->m_loop, false); } } void OutputPipeWire::onRegistryEventGlobal(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const spa_dict *props) { Q_UNUSED(id) Q_UNUSED(permissions) Q_UNUSED(version) OutputPipeWire *o = static_cast(data); if (qstrcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; auto media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); if (!media_class) return; if (qstrcmp(media_class, "Audio/Sink") != 0) return; o->m_hasSinks = true; o->m_coreInitSeq = pw_core_sync(o->m_core, PW_ID_CORE, o->m_coreInitSeq); } VolumePipeWire::VolumePipeWire() { QSettings settings(Qmmp::configFile(), QSettings::IniFormat); m_volume.left = settings.value("OutputPipeWire/left_volume", 100).toInt(); m_volume.right = settings.value("OutputPipeWire/right_volume", 100).toInt(); OutputPipeWire::volumeControl = this; } VolumePipeWire::~VolumePipeWire() { QSettings settings(Qmmp::configFile(), QSettings::IniFormat); settings.setValue("OutputPipeWire/left_volume", m_volume.left); settings.setValue("OutputPipeWire/right_volume", m_volume.right); OutputPipeWire::volumeControl = nullptr; } void VolumePipeWire::setVolume(const VolumeSettings &vol) { if(OutputPipeWire::instance) OutputPipeWire::instance->setVolume(vol); m_volume = vol; } VolumeSettings VolumePipeWire::volume() const { return m_volume; } bool VolumePipeWire::isMuted() const { return m_muted; } void VolumePipeWire::setMuted(bool mute) { if(OutputPipeWire::instance) OutputPipeWire::instance->setMuted(mute); m_muted = mute; } Volume::VolumeFlags VolumePipeWire::flags() const { return Volume::IsMuteSupported | Volume::HasNotifySignal; }