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