Skip to content

Commit da9f912

Browse files
AudioFlinger: add support for I2S microphone recording to WAV
1 parent e64b475 commit da9f912

File tree

8 files changed

+896
-17
lines changed

8 files changed

+896
-17
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "Sound Recorder",
3+
"publisher": "MicroPythonOS",
4+
"short_description": "Record audio from microphone",
5+
"long_description": "Record audio from the I2S microphone and save as WAV files. Recordings can be played back with the Music Player app.",
6+
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.soundrecorder/icons/com.micropythonos.soundrecorder_0.0.1_64x64.png",
7+
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.soundrecorder/mpks/com.micropythonos.soundrecorder_0.0.1.mpk",
8+
"fullname": "com.micropythonos.soundrecorder",
9+
"version": "0.0.1",
10+
"category": "utilities",
11+
"activities": [
12+
{
13+
"entrypoint": "assets/sound_recorder.py",
14+
"classname": "SoundRecorder",
15+
"intent_filters": [
16+
{
17+
"action": "main",
18+
"category": "launcher"
19+
}
20+
]
21+
}
22+
]
23+
}
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
# Sound Recorder App - Record audio from I2S microphone to WAV files
2+
import os
3+
import time
4+
5+
from mpos.apps import Activity
6+
import mpos.ui
7+
import mpos.audio.audioflinger as AudioFlinger
8+
9+
10+
def _makedirs(path):
11+
"""
12+
Create directory and all parent directories (like os.makedirs).
13+
MicroPython doesn't have os.makedirs, so we implement it manually.
14+
"""
15+
if not path:
16+
return
17+
18+
parts = path.split('/')
19+
current = ''
20+
21+
for part in parts:
22+
if not part:
23+
continue
24+
current = current + '/' + part if current else part
25+
try:
26+
os.mkdir(current)
27+
except OSError:
28+
pass # Directory may already exist
29+
30+
31+
class SoundRecorder(Activity):
32+
"""
33+
Sound Recorder app for recording audio from I2S microphone.
34+
Saves recordings as WAV files that can be played with Music Player.
35+
"""
36+
37+
# Constants
38+
MAX_DURATION_MS = 60000 # 60 seconds max recording
39+
RECORDINGS_DIR = "data/com.micropythonos.soundrecorder/recordings"
40+
41+
# UI Widgets
42+
_status_label = None
43+
_timer_label = None
44+
_record_button = None
45+
_record_button_label = None
46+
_play_button = None
47+
_play_button_label = None
48+
_delete_button = None
49+
_last_file_label = None
50+
51+
# State
52+
_is_recording = False
53+
_last_recording = None
54+
_timer_task = None
55+
_record_start_time = 0
56+
57+
def onCreate(self):
58+
screen = lv.obj()
59+
60+
# Title
61+
title = lv.label(screen)
62+
title.set_text("Sound Recorder")
63+
title.align(lv.ALIGN.TOP_MID, 0, 10)
64+
title.set_style_text_font(lv.font_montserrat_20, 0)
65+
66+
# Status label (shows microphone availability)
67+
self._status_label = lv.label(screen)
68+
self._status_label.align(lv.ALIGN.TOP_MID, 0, 40)
69+
70+
# Timer display
71+
self._timer_label = lv.label(screen)
72+
self._timer_label.set_text("00:00 / 01:00")
73+
self._timer_label.align(lv.ALIGN.CENTER, 0, -30)
74+
self._timer_label.set_style_text_font(lv.font_montserrat_24, 0)
75+
76+
# Record button
77+
self._record_button = lv.button(screen)
78+
self._record_button.set_size(120, 50)
79+
self._record_button.align(lv.ALIGN.CENTER, 0, 30)
80+
self._record_button.add_event_cb(self._on_record_clicked, lv.EVENT.CLICKED, None)
81+
82+
self._record_button_label = lv.label(self._record_button)
83+
self._record_button_label.set_text(lv.SYMBOL.AUDIO + " Record")
84+
self._record_button_label.center()
85+
86+
# Last recording info
87+
self._last_file_label = lv.label(screen)
88+
self._last_file_label.align(lv.ALIGN.BOTTOM_MID, 0, -70)
89+
self._last_file_label.set_text("No recordings yet")
90+
self._last_file_label.set_long_mode(lv.label.LONG_MODE.SCROLL_CIRCULAR)
91+
self._last_file_label.set_width(lv.pct(90))
92+
93+
# Play button
94+
self._play_button = lv.button(screen)
95+
self._play_button.set_size(80, 40)
96+
self._play_button.align(lv.ALIGN.BOTTOM_LEFT, 20, -20)
97+
self._play_button.add_event_cb(self._on_play_clicked, lv.EVENT.CLICKED, None)
98+
self._play_button.add_flag(lv.obj.FLAG.HIDDEN)
99+
100+
self._play_button_label = lv.label(self._play_button)
101+
self._play_button_label.set_text(lv.SYMBOL.PLAY + " Play")
102+
self._play_button_label.center()
103+
104+
# Delete button
105+
self._delete_button = lv.button(screen)
106+
self._delete_button.set_size(80, 40)
107+
self._delete_button.align(lv.ALIGN.BOTTOM_RIGHT, -20, -20)
108+
self._delete_button.add_event_cb(self._on_delete_clicked, lv.EVENT.CLICKED, None)
109+
self._delete_button.add_flag(lv.obj.FLAG.HIDDEN)
110+
111+
delete_label = lv.label(self._delete_button)
112+
delete_label.set_text(lv.SYMBOL.TRASH + " Delete")
113+
delete_label.center()
114+
115+
# Add to focus group
116+
focusgroup = lv.group_get_default()
117+
if focusgroup:
118+
focusgroup.add_obj(self._record_button)
119+
focusgroup.add_obj(self._play_button)
120+
focusgroup.add_obj(self._delete_button)
121+
122+
self.setContentView(screen)
123+
124+
def onResume(self, screen):
125+
super().onResume(screen)
126+
self._update_status()
127+
self._find_last_recording()
128+
129+
def onPause(self, screen):
130+
super().onPause(screen)
131+
# Stop recording if app goes to background
132+
if self._is_recording:
133+
self._stop_recording()
134+
135+
def _update_status(self):
136+
"""Update status label based on microphone availability."""
137+
if AudioFlinger.has_microphone():
138+
self._status_label.set_text("Microphone ready")
139+
self._status_label.set_style_text_color(lv.color_hex(0x00AA00), 0)
140+
self._record_button.remove_flag(lv.obj.FLAG.HIDDEN)
141+
else:
142+
self._status_label.set_text("No microphone available")
143+
self._status_label.set_style_text_color(lv.color_hex(0xAA0000), 0)
144+
self._record_button.add_flag(lv.obj.FLAG.HIDDEN)
145+
146+
def _find_last_recording(self):
147+
"""Find the most recent recording file."""
148+
try:
149+
# Ensure recordings directory exists
150+
_makedirs(self.RECORDINGS_DIR)
151+
152+
# List recordings
153+
files = os.listdir(self.RECORDINGS_DIR)
154+
wav_files = [f for f in files if f.endswith('.wav')]
155+
156+
if wav_files:
157+
# Sort by name (which includes timestamp)
158+
wav_files.sort(reverse=True)
159+
self._last_recording = f"{self.RECORDINGS_DIR}/{wav_files[0]}"
160+
self._last_file_label.set_text(f"Last: {wav_files[0]}")
161+
self._play_button.remove_flag(lv.obj.FLAG.HIDDEN)
162+
self._delete_button.remove_flag(lv.obj.FLAG.HIDDEN)
163+
else:
164+
self._last_recording = None
165+
self._last_file_label.set_text("No recordings yet")
166+
self._play_button.add_flag(lv.obj.FLAG.HIDDEN)
167+
self._delete_button.add_flag(lv.obj.FLAG.HIDDEN)
168+
169+
except Exception as e:
170+
print(f"SoundRecorder: Error finding recordings: {e}")
171+
self._last_recording = None
172+
173+
def _generate_filename(self):
174+
"""Generate a timestamped filename for the recording."""
175+
# Get current time
176+
t = time.localtime()
177+
timestamp = f"{t[0]:04d}-{t[1]:02d}-{t[2]:02d}_{t[3]:02d}-{t[4]:02d}-{t[5]:02d}"
178+
return f"{self.RECORDINGS_DIR}/{timestamp}.wav"
179+
180+
def _on_record_clicked(self, event):
181+
"""Handle record button click."""
182+
print(f"SoundRecorder: _on_record_clicked called, _is_recording={self._is_recording}")
183+
if self._is_recording:
184+
print("SoundRecorder: Stopping recording...")
185+
self._stop_recording()
186+
else:
187+
print("SoundRecorder: Starting recording...")
188+
self._start_recording()
189+
190+
def _start_recording(self):
191+
"""Start recording audio."""
192+
print("SoundRecorder: _start_recording called")
193+
print(f"SoundRecorder: has_microphone() = {AudioFlinger.has_microphone()}")
194+
195+
if not AudioFlinger.has_microphone():
196+
print("SoundRecorder: No microphone available - aborting")
197+
return
198+
199+
# Generate filename
200+
file_path = self._generate_filename()
201+
print(f"SoundRecorder: Generated filename: {file_path}")
202+
203+
# Start recording
204+
print(f"SoundRecorder: Calling AudioFlinger.record_wav()")
205+
print(f" file_path: {file_path}")
206+
print(f" duration_ms: {self.MAX_DURATION_MS}")
207+
print(f" sample_rate: 16000")
208+
209+
success = AudioFlinger.record_wav(
210+
file_path=file_path,
211+
duration_ms=self.MAX_DURATION_MS,
212+
on_complete=self._on_recording_complete,
213+
sample_rate=16000
214+
)
215+
216+
print(f"SoundRecorder: record_wav returned: {success}")
217+
218+
if success:
219+
self._is_recording = True
220+
self._record_start_time = time.ticks_ms()
221+
self._last_recording = file_path
222+
print(f"SoundRecorder: Recording started successfully")
223+
224+
# Update UI
225+
self._record_button_label.set_text(lv.SYMBOL.STOP + " Stop")
226+
self._record_button.set_style_bg_color(lv.color_hex(0xAA0000), 0)
227+
self._status_label.set_text("Recording...")
228+
self._status_label.set_style_text_color(lv.color_hex(0xAA0000), 0)
229+
230+
# Hide play/delete buttons during recording
231+
self._play_button.add_flag(lv.obj.FLAG.HIDDEN)
232+
self._delete_button.add_flag(lv.obj.FLAG.HIDDEN)
233+
234+
# Start timer update
235+
self._start_timer_update()
236+
else:
237+
print("SoundRecorder: record_wav failed!")
238+
self._status_label.set_text("Failed to start recording")
239+
self._status_label.set_style_text_color(lv.color_hex(0xAA0000), 0)
240+
241+
def _stop_recording(self):
242+
"""Stop recording audio."""
243+
AudioFlinger.stop()
244+
self._is_recording = False
245+
246+
# Update UI
247+
self._record_button_label.set_text(lv.SYMBOL.AUDIO + " Record")
248+
self._record_button.set_style_bg_color(lv.theme_get_color_primary(None), 0)
249+
self._update_status()
250+
251+
# Stop timer update
252+
self._stop_timer_update()
253+
254+
def _on_recording_complete(self, message):
255+
"""Callback when recording finishes."""
256+
print(f"SoundRecorder: {message}")
257+
258+
# Update UI on main thread
259+
self.update_ui_threadsafe_if_foreground(self._recording_finished, message)
260+
261+
def _recording_finished(self, message):
262+
"""Update UI after recording finishes (called on main thread)."""
263+
self._is_recording = False
264+
265+
# Update UI
266+
self._record_button_label.set_text(lv.SYMBOL.AUDIO + " Record")
267+
self._record_button.set_style_bg_color(lv.theme_get_color_primary(None), 0)
268+
self._update_status()
269+
self._find_last_recording()
270+
271+
# Stop timer update
272+
self._stop_timer_update()
273+
274+
def _start_timer_update(self):
275+
"""Start updating the timer display."""
276+
# Use LVGL timer for periodic updates
277+
self._timer_task = lv.timer_create(self._update_timer, 100, None)
278+
279+
def _stop_timer_update(self):
280+
"""Stop updating the timer display."""
281+
if self._timer_task:
282+
self._timer_task.delete()
283+
self._timer_task = None
284+
self._timer_label.set_text("00:00 / 01:00")
285+
286+
def _update_timer(self, timer):
287+
"""Update timer display (called periodically)."""
288+
if not self._is_recording:
289+
return
290+
291+
elapsed_ms = time.ticks_diff(time.ticks_ms(), self._record_start_time)
292+
elapsed_sec = elapsed_ms // 1000
293+
max_sec = self.MAX_DURATION_MS // 1000
294+
295+
elapsed_min = elapsed_sec // 60
296+
elapsed_sec = elapsed_sec % 60
297+
max_min = max_sec // 60
298+
max_sec_display = max_sec % 60
299+
300+
self._timer_label.set_text(
301+
f"{elapsed_min:02d}:{elapsed_sec:02d} / {max_min:02d}:{max_sec_display:02d}"
302+
)
303+
304+
def _on_play_clicked(self, event):
305+
"""Handle play button click."""
306+
if self._last_recording and not self._is_recording:
307+
# Stop any current playback
308+
AudioFlinger.stop()
309+
time.sleep_ms(100)
310+
311+
# Play the recording
312+
success = AudioFlinger.play_wav(
313+
self._last_recording,
314+
stream_type=AudioFlinger.STREAM_MUSIC,
315+
on_complete=self._on_playback_complete
316+
)
317+
318+
if success:
319+
self._status_label.set_text("Playing...")
320+
self._status_label.set_style_text_color(lv.color_hex(0x0000AA), 0)
321+
else:
322+
self._status_label.set_text("Playback failed")
323+
self._status_label.set_style_text_color(lv.color_hex(0xAA0000), 0)
324+
325+
def _on_playback_complete(self, message):
326+
"""Callback when playback finishes."""
327+
self.update_ui_threadsafe_if_foreground(self._update_status)
328+
329+
def _on_delete_clicked(self, event):
330+
"""Handle delete button click."""
331+
if self._last_recording and not self._is_recording:
332+
try:
333+
os.remove(self._last_recording)
334+
print(f"SoundRecorder: Deleted {self._last_recording}")
335+
self._find_last_recording()
336+
self._status_label.set_text("Recording deleted")
337+
except Exception as e:
338+
print(f"SoundRecorder: Delete failed: {e}")
339+
self._status_label.set_text("Delete failed")
340+
self._status_label.set_style_text_color(lv.color_hex(0xAA0000), 0)

0 commit comments

Comments
 (0)