This tutorial shows how to build a multilingual voice command page inside SoifGo WebView that converts your speech to text and sends it directly to a connected Bluetooth serial device (HC-05, HC-06, ESP32…).
Language selector + Start button + live recognized text
On the first run, SoifGo must ask the user for microphone permission. Call this Sub once — the app will show a native permission dialog. After the user allows it, the permission stays granted for all future sessions.
Sub request_mic_perm(PermissionRequest1 As Object)
' SoifGo automatically shows the native microphone permission dialog.
' The user's choice is remembered by Android.
End Sub
This is the complete page. When the user taps the microphone button, the script first
calls request_mic_perm and only then starts speech recognition.
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SoifGo Voice · Multilingual</title>
<style>
* { box-sizing: border-box; }
body {
font-family: Tahoma, sans-serif;
text-align: center;
padding: 20px;
background-color: #121212;
color: white;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.container {
max-width: 650px;
width: 100%;
background: #1a1a1a;
border-radius: 28px;
padding: 25px 20px 30px;
box-shadow: 0 12px 25px rgba(0, 0, 0, 0.7);
}
h2 {
margin-top: 0;
font-weight: 500;
letter-spacing: 0.5px;
color: #e0e0e0;
font-size: 1.8rem;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.lang-selector {
margin-bottom: 25px;
text-align: left;
background: #252525;
padding: 12px 18px;
border-radius: 60px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
width: 100%;
}
.lang-selector label {
font-size: 1rem;
color: #b0b0b0;
font-weight: 400;
display: flex;
align-items: center;
gap: 6px;
}
select {
background: #2e2e2e;
color: #f0f0f0;
border: 1px solid #3e3e3e;
border-radius: 40px;
padding: 12px 22px;
font-size: 1rem;
font-family: inherit;
font-weight: 500;
cursor: pointer;
outline: none;
flex: 1;
min-width: 180px;
max-width: 320px;
}
button {
padding: 16px 36px;
font-size: 1.2rem;
cursor: pointer;
background: #2196f3;
color: white;
border: none;
border-radius: 50px;
font-weight: bold;
margin-bottom: 18px;
width: 100%;
max-width: 320px;
}
button.listening {
background: #dc3545;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.85; }
100% { opacity: 1; }
}
#output {
margin-top: 12px;
font-size: 1.2rem;
color: #90caf9;
font-weight: 500;
padding: 18px 20px;
border: 2px dashed #2196f3;
background: #1e1e1e;
border-radius: 18px;
min-height: 70px;
word-break: break-word;
}
#status {
font-size: 0.9rem;
color: #aaa;
font-style: italic;
margin: 12px 0 5px;
min-height: 1.8rem;
}
</style>
</head>
<body>
<div class="container">
<h2>🎙️ SoifGo Voice Control</h2>
<div class="lang-selector">
<label for="lang-select">🌐 Language:</label>
<select id="lang-select">
<option value="en-US" selected>English</option>
<option value="es-ES">Español</option>
<option value="zh-CN">简体中文</option>
<option value="fr-FR">Français</option>
<option value="de-DE">Deutsch</option>
<option value="ja-JP">日本語</option>
<option value="ru-RU">Русский</option>
<option value="ar-SA">العربية</option>
<option value="fa-IR">فارسی</option>
<option value="it-IT">Italiano</option>
<option value="hi-IN">हिन्दी</option>
<option value="tr-TR">Türkçe</option>
<option value="ko-KR">한국어</option>
<option value="pt-BR">Português</option>
<option value="he-IL">עברית</option>
<option value="ku-IQ">کوردی</option>
<option value="ur-PK">اردو</option>
<option value="bn-BD">বাংলা</option>
<option value="id-ID">Bahasa Indonesia</option>
<option value="ta-IN">தமிழ்</option>
<option value="th-TH">ไทย</option>
<option value="sw-KE">Swahili</option>
</select>
</div>
<p id="status">Status: Ready for voice command</p>
<button id="start-btn">🎤 Start Speaking</button>
<div id="output">Your voice command will appear here...</div>
</div>
<script>
(function() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const startBtn = document.getElementById('start-btn');
const output = document.getElementById('output');
const status = document.getElementById('status');
const langSelect = document.getElementById('lang-select');
let isListening = false;
let recognition = null;
if (!SpeechRecognition) {
output.innerText = "⚠️ Speech recognition not supported.";
startBtn.disabled = true;
return;
}
recognition = new SpeechRecognition();
recognition.lang = 'en-US';
recognition.continuous = true;
recognition.interimResults = false;
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
const speechToText = event.results[event.resultIndex][0].transcript;
if (speechToText.trim() !== "") {
output.innerText = speechToText;
if (window.soifgo && typeof window.soifgo.CallSub === 'function') {
window.soifgo.CallSub('html_bluetooth_rx', true, speechToText);
status.innerText = "✅ Sent: " + speechToText;
} else {
status.innerText = "📤 (simulated) " + speechToText;
}
}
};
recognition.onerror = (event) => {
if (event.error === 'no-speech') {
status.innerText = "🎤 No speech detected, still waiting...";
} else if (event.error === 'not-allowed') {
status.innerText = "🚫 Microphone denied.";
} else {
status.innerText = "⚠️ Error: " + event.error;
}
};
recognition.onend = () => {
if (isListening) {
try { recognition.start(); } catch (e) {}
} else {
status.innerText = "Status: Stopped.";
}
};
startBtn.addEventListener('click', () => {
if (!isListening) {
// Request mic permission first
if (window.soifgo && typeof window.soifgo.CallSub === 'function') {
window.soifgo.CallSub('request_mic_perm', true, null);
}
isListening = true;
startBtn.innerText = "🛑 Stop Listening";
startBtn.classList.add('listening');
status.innerText = "🎧 Listening...";
output.innerText = "";
recognition.lang = langSelect.value;
try { recognition.start(); } catch (e) {}
} else {
isListening = false;
startBtn.innerText = "🎤 Start Speaking";
startBtn.classList.remove('listening');
recognition.stop();
status.innerText = "Status: Stopped.";
}
});
langSelect.addEventListener('change', (e) => {
const newLang = e.target.value;
if (recognition) {
if (isListening) {
recognition.stop();
recognition.lang = newLang;
} else {
recognition.lang = newLang;
}
}
});
})();
</script>
</body>
</html>
request_mic_perm is called inside the
startBtn.addEventListener('click', …) handler. If you want SoifGo to
actually show the permission dialog, you must add this single line at the beginning
of that handler:
window.soifgo.CallSub('request_mic_perm', true, null);
The current example already works, but adding this call makes the permission request
explicit and prevents the "not-allowed" error on the first run.
After speech recognition returns a transcript, JavaScript calls SoifGo's Bluetooth-send Sub with one line:
window.soifgo.CallSub('html_bluetooth_rx', true, speechToText);
The full flow is:
request_mic_perm is triggered.SpeechRecognition converts voice to text.window.soifgo.CallSub('html_bluetooth_rx', true, txt).html_bluetooth_rx() Sub.recognition.onresult = (event) => {
const speechToText = event.results[event.resultIndex][0].transcript;
if (speechToText.trim() !== "") {
output.innerText = speechToText;
window.soifgo.CallSub('html_bluetooth_rx', true, speechToText);
}
};
Sub html_bluetooth_rx(txt As String)
' Send the text to the connected Bluetooth serial device.
BluetoothSend(txt)
End Sub
If your hardware sends data back (e.g. sensor values, ACK messages), define a global
JavaScript function named html_bluetooth_tx. SoifGo calls it automatically
whenever Bluetooth data arrives:
function html_bluetooth_tx(data) {
document.getElementById('output').innerText = data;
}
window.html_bluetooth_tx = html_bluetooth_tx; // register globally
serial_voice.html.
It is already available inside the SoifGo library — no download needed.
request_mic_perm is called and Android asks for
microphone permission — tap Allow. Next times, no prompt appears.
The page includes a language selector with 22 languages. Changing the language restarts speech recognition with the new locale automatically:
English · Español · 简体中文 · Français · Deutsch · 日本語 · Русский · العربية · فارسی · Italiano · हिन्दी · Türkçe · 한국어 · Português · עברית · کوردی · اردو · বাংলা · Bahasa Indonesia · தமிழ் · ไทย · Swahili
onend, so you can speak multiple
commands without pressing the button again.serial_voice.html — feel free to copy and modify it.This simple sketch receives text over Bluetooth serial and turns the built-in LED (pin 13) on or off. It is the fastest way to confirm that your voice commands are actually reaching the hardware.
/*
* SoifGo Voice -> Serial Bluetooth -> LED
* ----------------------------------------
* Listens for the phrases "LAMP ON" and "LAMP OFF"
* and controls the built-in LED on pin 13.
*
* Wiring:
* Arduino pin 13 -> built-in LED (no external parts needed)
* HC-05 / HC-06 RX -> Arduino TX (pin 1) [via voltage divider if 5V board]
* HC-05 / HC-06 TX -> Arduino RX (pin 0)
* HC-05 VCC -> 5V, GND -> GND
*
* Baud rate: 9600
*/
const int LED_PIN = 13;
String incoming = "";
void setup() {
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.begin(9600);
Serial.println("Ready. Waiting for voice commands...");
}
void loop() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (incoming.length() > 0) {
handleCommand(incoming);
incoming = "";
}
} else {
incoming += c;
}
}
}
void handleCommand(String cmd) {
cmd.trim();
cmd.toUpperCase();
Serial.print("Received: ");
Serial.println(cmd);
if (cmd == "LAMP ON" || cmd == "LED ON" || cmd == "ON") {
digitalWrite(LED_PIN, HIGH);
Serial.println("-> LED is now ON");
}
else if (cmd == "LAMP OFF" || cmd == "LED OFF" || cmd == "OFF") {
digitalWrite(LED_PIN, LOW);
Serial.println("-> LED is now OFF");
}
else {
Serial.println("-> Unknown command.");
}
}
serial_voice.html page and tap the microphone."LAMP ON" with "ON" and
"LAMP OFF" with "OFF" in both the Arduino sketch
and your speech. The sketch above already accepts "ON" and
"OFF" as valid alternatives.
LED_PIN to 2 (the built-in LED on most ESP32 dev boards)
or to LED_BUILTIN. The rest of the sketch works unchanged.
request_mic_perm for microphone access.window.soifgo.CallSub('html_bluetooth_rx', true, txt).html_bluetooth_tx(data).