Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 106 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ jobs:
curl -O $base_src/rnn.c
curl -O $base_src/rnn_data.c

gcc -shared -o rnnoise.dll -O3 -std=c99 \
gcc -shared -o rnnoise.dll -O3 -std=gnu99 \
-D_USE_MATH_DEFINES \
celt_lpc.c denoise.c kiss_fft.c pitch.c rnn.c rnn_data.c \
-I.
Expand Down Expand Up @@ -156,9 +156,107 @@ jobs:
name: MicRouter-PC-Windows
path: release_package/MicRouter-PC-Windows.zip

build-linux:
needs: test-python
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true

- name: Install Build Dependencies
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libgtk-3-dev libportaudio2 portaudio19-dev liblzma-dev libstdc++-12-dev

- name: Build Flutter Linux App
working-directory: desktop/frontend
run: flutter build linux --release

- name: Compile RNNoise Library (Linux)
run: |
mkdir rnnoise_build
cd rnnoise_build

base_src="https://raw.githubusercontent.com/xiph/rnnoise/master/src"
base_inc="https://raw.githubusercontent.com/xiph/rnnoise/master/include"

curl -O $base_inc/rnnoise.h
curl -O $base_src/celt_lpc.h
curl -O $base_src/kiss_fft.h
curl -O $base_src/pitch.h
curl -O $base_src/rnn.h
curl -O $base_src/rnn_data.h
curl -O $base_src/tansig_table.h
curl -O $base_src/_kiss_fft_guts.h
curl -O $base_src/arch.h
curl -O $base_src/common.h
curl -O $base_src/opus_types.h

curl -O $base_src/celt_lpc.c
curl -O $base_src/denoise.c
curl -O $base_src/kiss_fft.c
curl -O $base_src/pitch.c
curl -O $base_src/rnn.c
curl -O $base_src/rnn_data.c

gcc -shared -o rnnoise.so -O3 -fPIC -std=gnu99 \
-D_GNU_SOURCE -D_USE_MATH_DEFINES \
celt_lpc.c denoise.c kiss_fft.c pitch.c rnn.c rnn_data.c \
-I. -lm

- name: Create Debian Package Structure
run: |
mkdir -p deb_root/usr/bin
mkdir -p deb_root/usr/lib/microuter/backend/libs
mkdir -p deb_root/DEBIAN

# App binary and assets
cp -r desktop/frontend/build/linux/x64/release/bundle/* deb_root/usr/lib/microuter/

# Launcher script
cat > deb_root/usr/bin/microuter <<EOF
#!/bin/bash
cd /usr/lib/microuter
./microuter_pc "\$@"
EOF
chmod +x deb_root/usr/bin/microuter

# Backend
cp desktop/backend/backend.py deb_root/usr/lib/microuter/backend/
cp desktop/backend/denoiser.py deb_root/usr/lib/microuter/backend/
cp desktop/backend/requirements.txt deb_root/usr/lib/microuter/backend/
cp rnnoise_build/rnnoise.so deb_root/usr/lib/microuter/backend/libs/

# Control file
cat > deb_root/DEBIAN/control <<EOF
Package: microuter
Version: 1.0.0
Section: utils
Priority: optional
Architecture: amd64
Depends: python3, python3-pip, python3-numpy, python3-pyaudio, libportaudio2, libgtk-3-0 | libgtk-3-0t64
Maintainer: MicRouter Team
Description: Use your Android phone as a microphone for your PC.
EOF

- name: Build Debian Package
run: dpkg-deb --build deb_root microuter_1.0.0_amd64.deb

- name: Upload Debian Package
uses: actions/upload-artifact@v4
with:
name: MicRouter-PC-Linux-Deb
path: microuter_1.0.0_amd64.deb

release:
runs-on: ubuntu-latest
needs: [build_android, build-windows]
needs: [build_android, build-windows, build-linux]
if: github.event_name == 'push'
steps:
- name: Checkout code
Expand All @@ -176,6 +274,11 @@ jobs:
with:
name: MicRouter-PC-Windows

- name: Download Linux deb package
uses: actions/download-artifact@v4
with:
name: MicRouter-PC-Linux-Deb

- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
Expand All @@ -191,5 +294,6 @@ jobs:
files: |
app-release.apk
MicRouter-PC-Windows.zip
microuter_1.0.0_amd64.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions android/app/src/main/java/com/mmd/microuter/AudioService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ class AudioService : Service() {
AppLogger.i("AudioService", "NoiseSuppressor enabled")
} catch (e: Exception) {
AppLogger.w("AudioService", "NoiseSuppressor failed: ${e.message}")
suppressor?.release()
suppressor = null
}
}

Expand All @@ -323,6 +325,8 @@ class AudioService : Service() {
AppLogger.i("AudioService", "AcousticEchoCanceler enabled")
} catch (e: Exception) {
AppLogger.w("AudioService", "AcousticEchoCanceler failed: ${e.message}")
echo?.release()
echo = null
}
}
}
Expand Down
52 changes: 36 additions & 16 deletions desktop/backend/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,25 @@ def _scan_devices(self):
"""
self.device_map = {}

# 1. Find the WASAPI Host API Index
wasapi_index = -1
# 1. Find the Low Latency Host API Index (WASAPI for Win, Pulse/ALSA for Linux)
target_api_index = -1
is_windows = sys.platform == "win32"
target_api_name = "WASAPI" if is_windows else "PulseAudio"

try:
for i in range(self.p.get_host_api_count()):
api = self.p.get_host_api_info_by_index(i)
if "WASAPI" in api.get("name", ""):
wasapi_index = i
if target_api_name in api.get("name", ""):
target_api_index = i
break

# Fallback for Linux if PulseAudio is not found
if not is_windows and target_api_index == -1:
for i in range(self.p.get_host_api_count()):
api = self.p.get_host_api_info_by_index(i)
if "ALSA" in api.get("name", ""):
target_api_index = i
break
except: pass

# 2. Get total device count
Expand Down Expand Up @@ -105,8 +116,8 @@ def _scan_devices(self):
# Clean the name for matching (WASAPI devices often prepend "Speakers (Realtek...)")
clean_name = name

if host_api == wasapi_index:
# This is a WASAPI device. Always prefer this.
if host_api == target_api_index:
# This is a Low Latency device. Always prefer this.
# We might overwrite a previous entry with the same name, which is good.
self.device_map[name] = i

Expand Down Expand Up @@ -152,15 +163,19 @@ def process_command(self, cmd):

elif command == 'toggle_rnnoise':
self.use_rnnoise = cmd.get('value', False)
if self.use_rnnoise and self.rnnoise is None:
try:
self.rnnoise = RNNoise()
self.send_to_flutter({"type": "log", "message": "[*] AI Denoising Enabled"})
except Exception as e:
self.send_to_flutter({"type": "error", "message": f"RNNoise Error: {e}"})
self.use_rnnoise = False
elif not self.use_rnnoise:
self.send_to_flutter({"type": "log", "message": "[*] AI Denoising Disabled"})
if self.use_rnnoise:
if self.rnnoise is None:
try:
self.rnnoise = RNNoise()
self.send_to_flutter({"type": "log", "message": "[*] AI Denoising Enabled"})
except Exception as e:
self.send_to_flutter({"type": "error", "message": f"RNNoise Error: {e}"})
self.use_rnnoise = False
else:
if self.rnnoise is not None:
self.rnnoise.destroy()
self.rnnoise = None
self.send_to_flutter({"type": "log", "message": "[*] AI Denoising Disabled"})

elif command == 'start':
if not self.is_streaming:
Expand Down Expand Up @@ -353,7 +368,7 @@ def audio_stream_logic(self, device_name, port):
final_data = audio_array.tobytes()

# Send RMS to UI (Throttle to avoid flooding socket)
if len(audio_array) > 0 and int(time.time() * 10) % 2 == 0:
if len(audio_array) > 0 and int(time.time() * 20) % 2 == 0:
rms = np.sqrt(np.mean(audio_array.astype(float)**2))
self.send_to_flutter({"type": "volume", "value": min(rms / 2000, 1.0)})

Expand Down Expand Up @@ -384,8 +399,13 @@ def audio_stream_logic(self, device_name, port):
def cleanup(self):
self.is_streaming = False
try:
if self.rnnoise:
self.rnnoise.destroy()
self.rnnoise = None
self.p.terminate()
self.server_socket.close()
if self.client_socket:
self.client_socket.close()
except: pass

if __name__ == "__main__":
Expand Down
9 changes: 7 additions & 2 deletions desktop/backend/denoiser.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,10 @@ def process(self, chunk_bytes):
return bytes(output_bytes)

def destroy(self):
self.lib.rnnoise_destroy.argtypes = [ctypes.c_void_p]
self.lib.rnnoise_destroy(self.state)
if hasattr(self, 'state') and self.state:
self.lib.rnnoise_destroy.argtypes = [ctypes.c_void_p]
self.lib.rnnoise_destroy(self.state)
self.state = None

def __del__(self):
self.destroy()
12 changes: 6 additions & 6 deletions desktop/frontend/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.

version:
revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4"
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
channel: "stable"

project_type: app
Expand All @@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
- platform: windows
create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: linux
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a

# User provided section

Expand Down
17 changes: 17 additions & 0 deletions desktop/frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# microuter_pc

A new Flutter project.

## Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
35 changes: 21 additions & 14 deletions desktop/frontend/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class BackendController extends ChangeNotifier {
List<String> devices = [];
String? selectedDevice;

String _socketBuffer = "";
final List<int> _socketBytesBuffer = [];

BackendController() {
_startEmbeddedBackend();
Expand Down Expand Up @@ -85,8 +85,10 @@ class BackendController extends ChangeNotifier {
// Check if script exists (Release mode vs Debug mode adjustments might be needed)
if (await File(scriptPath).exists()) {
try {
_pythonProcess = await Process.start('python', [scriptPath]);
_log("Python backend started.");
// Use 'python3' on Linux/macOS as 'python' often doesn't exist
String pythonCmd = Platform.isWindows ? 'python' : 'python3';
_pythonProcess = await Process.start(pythonCmd, [scriptPath]);
_log("Python backend started using $pythonCmd.");

// Listen to Python's STDERR for debugging
_pythonProcess!.stderr.transform(utf8.decoder).listen((data) {
Expand Down Expand Up @@ -123,7 +125,7 @@ class BackendController extends ChangeNotifier {
// Request initial state
sendCommand("get_devices");

_socket!.cast<List<int>>().transform(utf8.decoder).listen(
_socket!.listen(
_onDataReceived,
onDone: () {
status = "Backend Disconnected";
Expand All @@ -149,20 +151,25 @@ class BackendController extends ChangeNotifier {
Future.delayed(const Duration(seconds: 2), connectToPython);
}

// --- CRITICAL FIX: Handle Fragmented TCP Packets ---
void _onDataReceived(String data) {
_socketBuffer += data;
// --- CRITICAL FIX: Handle Fragmented TCP Packets (Byte-level) ---
void _onDataReceived(List<int> data) {
_socketBytesBuffer.addAll(data);

while (_socketBuffer.contains('\n')) {
int index = _socketBuffer.indexOf('\n');
String line = _socketBuffer.substring(0, index).trim();
_socketBuffer = _socketBuffer.substring(index + 1);
while (true) {
int index = _socketBytesBuffer.indexOf(10); // 10 is '\n'
if (index == -1) break;

if (line.isNotEmpty) {
List<int> lineBytes = _socketBytesBuffer.sublist(0, index);
_socketBytesBuffer.removeRange(0, index + 1);

if (lineBytes.isNotEmpty) {
try {
_handleMessage(jsonDecode(line));
String line = utf8.decode(lineBytes).trim();
if (line.isNotEmpty) {
_handleMessage(jsonDecode(line));
}
} catch (e) {
print("JSON Error: $e | Line: $line");
print("Socket Data Error: $e");
}
}
}
Expand Down
1 change: 1 addition & 0 deletions desktop/frontend/linux/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
flutter/ephemeral
Loading
Loading