9
9
# github: https://github.com/jsalsman/webrec
10
10
11
11
from flask import Flask , request , render_template , redirect , send_from_directory
12
- from flask_socketio import SocketIO
12
+ from flask_socketio import SocketIO , emit # Fails over to POST submissions
13
13
import sox # needs command line sox and the pysox package
14
+ import lameenc # to produce .mp3 files for playback
14
15
from datetime import datetime # for audio file timestamps
15
16
import os # to delete old audio files
16
17
from time import time # to keep files less than 10 minutes old
17
18
18
- from sys import stderr # best for Replit; you may want to import logging
19
+ from sys import stderr # best for Replit; you may want to ' import logging'
19
20
log = lambda message : stderr .write (message + '\n ' ) # ...and connect this
20
21
21
22
app = Flask (__name__ )
22
- socketio = SocketIO (app )
23
+ socketio = SocketIO (app ) # Websocket
23
24
24
25
@app .route ('/' ) # redirect from / to /record
25
26
def index ():
@@ -40,48 +41,73 @@ def upload_audio():
40
41
41
42
timestamp = datetime .now ().strftime ("%M%S%f" )[:8 ] # MMSSssss
42
43
raw_filename = f"audio-{ timestamp } .raw"
43
- wav_filename = f"audio-{ timestamp } .wav"
44
44
45
45
audio_file .save ('static/' + raw_filename )
46
46
47
- # Convert format, trim silence
48
- tfm = sox .Transformer ()
49
- tfm .set_input_format (file_type = 'raw' , rate = 16000 , bits = 16 ,
50
- channels = 1 , encoding = 'signed-integer' )
51
-
52
- tfm .silence (min_silence_duration = 0.25 , # remove lengthy silence
53
- buffer_around_silence = True ) # replace removals with 1/4 second
54
- # https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.silence
55
-
56
- tfm .build ('static/' + raw_filename , 'static/' + wav_filename )
57
- duration = sox .file_info .duration ('static/' + wav_filename )
58
-
59
- # Clean up older files; maximum 40 MB will remain
60
- files = [os .path .join ('static' , f ) for f in
61
- os .listdir ('static' ) if f .startswith ('audio-' )]
62
- # Sort files by last modified time, oldest first
63
- files .sort (key = lambda x : os .path .getmtime (x ))
64
- current_time = time ()
65
- # Remove all but the 10 most recent audio files
66
- for file in files [:- 10 ]:
67
- # Get the modification time of the file
68
- mod_time = os .path .getmtime (file )
69
- # Calculate the age of the file in seconds
70
- file_age = current_time - mod_time
71
- # Check if the file is older than 10 minutes
72
- if file_age > 600 :
73
- os .remove (file )
74
- audio_space = sum ([os .path .getsize ('static/' + f )
75
- for f in os .listdir ('static' )
76
- if f .startswith ('audio-' )]) / (1024 ** 2 )
77
-
78
- log (f'Built { wav_filename } ({ duration :.1f} seconds.) ' +
79
- f'All audio using { audio_space :.2f} MB.' )
80
-
81
- return redirect (f'/playback/{ wav_filename } ' )
47
+ return redirect (f'/playback/' + process_file (raw_filename ))
82
48
83
49
return "No audio file" , 400
84
50
51
+ def process_file (raw_filename ):
52
+ # Convert format, trim silence
53
+ tfm = sox .Transformer ()
54
+ tfm .set_input_format (file_type = 'raw' , rate = 16000 , bits = 16 ,
55
+ channels = 1 , encoding = 'signed-integer' )
56
+
57
+ tfm .silence (min_silence_duration = 0.25 , # remove lengthy silence
58
+ buffer_around_silence = True ) # replace removals with 1/4 second
59
+ # https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.silence
60
+
61
+ #pcm = tfm.build_array('static/' + raw_filename) # FAILS
62
+ # sox/transform.py", line 793, in build_array
63
+ # encoding_out = [
64
+ # IndexError: list index out of range
65
+
66
+ tfm .build ('static/' + raw_filename , 'static/tmp-' + raw_filename )
67
+
68
+ # Set up the MP3 encoder
69
+ encoder = lameenc .Encoder ()
70
+ encoder .set_in_sample_rate (16000 )
71
+ encoder .set_channels (1 )
72
+ encoder .set_bit_rate (64 ) # https://github.com/chrisstaite/lameenc/blob/main/lameenc.c
73
+ encoder .set_quality (2 ) # https://github.com/gypified/libmp3lame/blob/master/include/lame.h
74
+
75
+ # Encode the PCM data to MP3
76
+ with open ('static/tmp-' + raw_filename , 'rb' ) as f :
77
+ mp3_data = encoder .encode (f .read ())
78
+ mp3_data += encoder .flush ()
79
+ os .remove ('static/tmp-' + raw_filename )
80
+
81
+ mp3_fn = raw_filename .replace ('.raw' , '.mp3' )
82
+
83
+ with open ('static/' + mp3_fn , 'wb' ) as f :
84
+ f .write (mp3_data )
85
+
86
+ duration = sox .file_info .duration ('static/' + mp3_fn )
87
+
88
+ # Clean up older files; maximum 40 MB will remain
89
+ files = [os .path .join ('static' , f ) for f in
90
+ os .listdir ('static' ) if f .startswith ('audio-' )]
91
+ # Sort files by last modified time, oldest first
92
+ files .sort (key = lambda x : os .path .getmtime (x ))
93
+ current_time = time ()
94
+ # Remove all but the 10 most recent audio files
95
+ for file in files [:- 10 ]:
96
+ # Get the modification time of the file
97
+ mod_time = os .path .getmtime (file )
98
+ # Calculate the age of the file in seconds
99
+ file_age = current_time - mod_time
100
+ # Check if the file is older than 10 minutes
101
+ if file_age > 600 :
102
+ os .remove (file )
103
+ audio_space = sum ([os .path .getsize ('static/' + f )
104
+ for f in os .listdir ('static' )
105
+ if f .startswith ('audio-' )]) / (1024 ** 2 )
106
+
107
+ log (f'Built { mp3_fn } ({ duration :.1f} seconds.) ' +
108
+ f'All audio using { audio_space :.2f} MB.' )
109
+ return mp3_fn
110
+
85
111
@app .route ('/playback/<filename>' )
86
112
def playback (filename ):
87
113
return render_template ('playback.html' , audio = filename )
@@ -98,7 +124,43 @@ def send_js(path):
98
124
for file in [os .path .join ('static' , f ) for f in os .listdir ('static' )
99
125
if f .startswith ('audio-' )]:
100
126
os .remove (file )
101
-
102
- app .run (host = '0.0.0.0' , port = 81 )
103
- # TODO: production WSGI server
127
+
128
+ # WebSocket implementation
129
+ active_streams = {}
130
+ sid_to_filename = {}
131
+
132
+ @socketio .on ('connect' )
133
+ def websocket_connect ():
134
+ timestamp = datetime .now ().strftime ("%H%M%S%f" )[:8 ]
135
+ sid_to_filename [request .sid ] = f"audio-{ timestamp } .raw"
136
+
137
+ @socketio .on ('audio_chunk' )
138
+ def websocket_chunk (data ):
139
+ try :
140
+ if request .sid not in active_streams :
141
+ filename = sid_to_filename [request .sid ]
142
+ active_streams [request .sid ] = open (f'static/{ filename } ' , 'wb' )
143
+ active_streams [request .sid ].write (data )
144
+ except Exception as e :
145
+ log (f"Error writing audio data: { e } " )
146
+ return 'fail' , repr (e )
147
+
148
+ @socketio .on ('end_recording' )
149
+ def websocket_end ():
150
+ try :
151
+ if request .sid in active_streams :
152
+ active_streams [request .sid ].close ()
153
+ filename = sid_to_filename [request .sid ]
154
+ mp3_fn = process_file (filename ) # See above
155
+ del active_streams [request .sid ]
156
+ del sid_to_filename [request .sid ]
157
+ return '/playback/' + mp3_fn
158
+ except Exception as e :
159
+ log (f"Error ending websocket: { e } " )
160
+ return 'fail' , repr (e )
161
+
162
+ #app.run(host='0.0.0.0', port=81)
163
+ socketio .run (app , host = '0.0.0.0' , port = 81 )
164
+
165
+ # TODO? production WSGI server
104
166
# see https://replit.com/talk/learn/How-to-set-up-production-environment-for-your-Flask-project-on-Replit/139169
0 commit comments