Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
lincot committed Sep 10, 2024
0 parents commit e195a46
Show file tree
Hide file tree
Showing 17 changed files with 1,193 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/CMakeCache.txt
/cmake_install.cmake
/CMakeFiles
/Makefile
/*autogen
/build
/target
/.cache
/.qt
/compile_commands.json
/vgcore*
/*.log
/*.supp
/PKGBUILD*
/*.tar.gz
33 changes: 33 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.16)

project(mic-indicator LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_compile_options(-fno-exceptions -fno-rtti)

find_package(Qt6 REQUIRED COMPONENTS Widgets)
find_package(PkgConfig REQUIRED)
pkg_search_module(PIPEWIRE REQUIRED libpipewire-0.3)

file(GLOB_RECURSE SRCS CONFIGURE_DEPENDS "src/*.cpp")

add_executable(${PROJECT_NAME} ${SRCS})

target_link_libraries(${PROJECT_NAME} Qt6::Widgets)
target_include_directories(${PROJECT_NAME} PRIVATE ${PIPEWIRE_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${PIPEWIRE_LIBRARIES})

include(GNUInstallDirs)
install(TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

set_target_properties(${PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# PipeWire microphone tray indicator

A simple microphone indicator for Linux using PipeWire, displayed
in the system tray. It shows if any applications are using the microphone
and if you can be heard.

![showcase](showcase.gif)

## Usage

```
Usage: mic-indicator [options]
PipeWire microphone tray indicator
Options:
-h, --help Displays help on commandline options.
--help-all Displays help, including generic Qt
options.
-e, --exclude-nodes <nodes> Comma-separated list of input node names
to ignore (default:
mic-indicator,capture.rnnoise_source)
-v, --volume-threshold <threshold> The volume threshold for speech
detection (default: 0.001)
-s, --sampling-rate <sampling rate> Sampling rate (default: 48000)
-c, --channels <channels> Number of channels (default: 1)
```

## Installation

### Manual installation

Run these commands:

```sh
git clone https://github.com/lincot/mic-indicator
cd mic-indicator
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-march=native"
cmake --build build
sudo cp build/mic-indicator /usr/bin/
sudo cp res/icons/* /usr/share/icons/hicolor/64x64/apps/
```
Binary file added res/icons/mic-indicator-high.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/mic-indicator-low.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/mic-indicator-off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added showcase.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions src/AudioProcessor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include "AudioProcessor.h"
#include "PipeWireManager.h"

AudioProcessor::AudioProcessor(PipeWireManager const *pipeWireManager,
float volumeThreshold)
: m_volumeThreshold(volumeThreshold), m_lowCount(0) {

connect(&m_timer, &QTimer::timeout, this, &AudioProcessor::processAudio,
Qt::DirectConnection);
connect(
pipeWireManager, &PipeWireManager::onMicUsageChanged, &m_timer,
[this](bool micActive) {
if (micActive) {
m_timer.start(100);
} else {
m_timer.stop();
}
},
Qt::QueuedConnection);
}

void AudioProcessor::start(pw_stream *stream) { m_stream = stream; }

float calculateRMS(const float *const audioSamples,
const uint32_t sampleCount) {
float sum = 0;
for (uint32_t i = 0; i < sampleCount; ++i) {
sum += audioSamples[i] * audioSamples[i];
}
return sqrtf(sum / static_cast<float>(sampleCount));
}

void AudioProcessor::processAudio() {
pw_buffer *buffer;
spa_buffer *spaBuffer;

buffer = pw_stream_dequeue_buffer(m_stream);
if (buffer == nullptr) {
return;
}

spaBuffer = buffer->buffer;
if (spaBuffer->datas[0].data == nullptr) {
pw_stream_queue_buffer(m_stream, buffer);
return;
}

const auto *audioSamples = static_cast<float *>(spaBuffer->datas[0].data);
const uint32_t sampleCount = spaBuffer->datas[0].chunk->size / sizeof(float);

const auto volume = calculateRMS(audioSamples, sampleCount);

pw_stream_queue_buffer(m_stream, buffer);

if (volume < m_volumeThreshold) {
if (++m_lowCount == 5) {
emit onVolumeChanged(false);
}
} else {
m_lowCount = 0;
emit onVolumeChanged(true);
}
}
27 changes: 27 additions & 0 deletions src/AudioProcessor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include "PipeWireManager.h"

// NOLINTNEXTLINE
class AudioProcessor : public QObject {
Q_OBJECT

public:
AudioProcessor(PipeWireManager const *pipeWireManager, float volumeThreshold);

signals:
void onVolumeChanged(bool highVolume);

public slots:
void start(pw_stream *stream);

private slots:
void processAudio();

// NOLINTNEXTLINE
private:
pw_stream *m_stream;
float m_volumeThreshold;
QTimer m_timer;
uint m_lowCount;
};
40 changes: 40 additions & 0 deletions src/Config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "Config.h"
#include <QCommandLineParser>

Config getConfig(const QApplication &app) {
QCommandLineParser parser;
parser.setApplicationDescription("PipeWire microphone tray indicator");
parser.addHelpOption();

const QCommandLineOption excludedNodesOption(
QStringList() << "e" << "exclude-nodes",
"Comma-separated list of input node names to ignore (default: "
"mic-indicator,capture.rnnoise_source)",
"nodes", "mic-indicator,capture.rnnoise_source");
parser.addOption(excludedNodesOption);

const QCommandLineOption volumeThresholdOption(
QStringList() << "v" << "volume-threshold",
"The volume threshold for speech detection (default: 0.001)", "threshold",
"0.001");
parser.addOption(volumeThresholdOption);

const QCommandLineOption samplingRateOption(
QStringList() << "s" << "sampling-rate", "Sampling rate (default: 48000)",
"sampling rate", "48000");
parser.addOption(samplingRateOption);

const QCommandLineOption channelsOption(QStringList() << "c" << "channels",
"Number of channels (default: 1)",
"channels", "1");
parser.addOption(channelsOption);

parser.process(app);

return Config{
.excludedNodes = parser.value(excludedNodesOption).split(','),
.volumeThreshold = parser.value(volumeThresholdOption).toFloat(),
.samplingRate = parser.value(samplingRateOption).toUInt(),
.channels = parser.value(channelsOption).toUInt(),
};
}
12 changes: 12 additions & 0 deletions src/Config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include <QApplication>

struct Config {
QStringList excludedNodes;
float volumeThreshold;
uint32_t samplingRate;
uint32_t channels;
};

Config getConfig(const QApplication &app);
57 changes: 57 additions & 0 deletions src/MicTrayIcon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include "MicTrayIcon.h"
#include <QApplication>

MicTrayIcon::MicTrayIcon()
: m_exitAction("Exit"), m_offIcon(QIcon::fromTheme("mic-indicator-off")),
m_lowVolumeIcon(QIcon::fromTheme("mic-indicator-low")),
m_highVolumeIcon(QIcon::fromTheme("mic-indicator-high")),
m_micActive(false), m_volumeHigh(false) {

setMicStatus(Off);
m_trayIcon.show();

QObject::connect(&m_exitAction, &QAction::triggered, qApp,
&QApplication::exit, Qt::DirectConnection);
m_menu.addAction(&m_exitAction);
m_trayIcon.setContextMenu(&m_menu);
}

void MicTrayIcon::setMicActive(bool micActive) {
m_micActive = micActive;
if (micActive) {
setMicStatus(Low);
} else {
setMicStatus(Off);
m_volumeHigh = false;
}
}

void MicTrayIcon::setVolumeHigh(bool volumeHigh) {
if (m_volumeHigh == volumeHigh || !m_micActive) {
return;
}

m_volumeHigh = volumeHigh;
if (volumeHigh) {
setMicStatus(High);
} else {
setMicStatus(Low);
}
}

void MicTrayIcon::setMicStatus(const MicStatus status) {
switch (status) {
case Off:
m_trayIcon.setIcon(m_offIcon);
m_trayIcon.setToolTip("Microphone is not in use");
break;
case Low:
m_trayIcon.setIcon(m_lowVolumeIcon);
m_trayIcon.setToolTip("Microphone is in use, no speech detected");
break;
case High:
m_trayIcon.setIcon(m_highVolumeIcon);
m_trayIcon.setToolTip("Microphone is in use, speech detected");
break;
}
}
36 changes: 36 additions & 0 deletions src/MicTrayIcon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

#include <QIcon>
#include <QMenu>
#include <QSystemTrayIcon>

enum MicStatus {
Off,
Low,
High,
};

class MicTrayIcon : public QObject {
Q_OBJECT

public:
MicTrayIcon();

// NOLINTNEXTLINE
public slots:
void setMicActive(bool micActive);
void setVolumeHigh(bool volumeHigh);

private:
void setMicStatus(MicStatus micStatus);

QSystemTrayIcon m_trayIcon;
QMenu m_menu;
QAction m_exitAction;
QIcon m_offIcon;
QIcon m_lowVolumeIcon;
QIcon m_highVolumeIcon;

bool m_micActive;
bool m_volumeHigh;
};
Loading

0 comments on commit e195a46

Please sign in to comment.