Skip to content

Commit cf30702

Browse files
committed
make audio compression optional via API param on POST speakers/ request so that we can optionally use the aggressive compression which makes sense for uploads from mobile devices, but not for admin uploads
1 parent 89a18a2 commit cf30702

File tree

6 files changed

+256
-8
lines changed

6 files changed

+256
-8
lines changed

roundware/api2/tests/test_audio_normalization.py

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,215 @@ def test_speakers_api_integration_disabled(self, temp_dir):
327327
mock_convert.assert_called_once()
328328

329329
# Should return the expected URL (with timestamp in filename)
330-
assert ".mp3" in result
330+
assert ".mp3" in result
331+
332+
333+
class TestAudioCompressionParameter:
334+
"""Test cases for the new audio_compression parameter functionality"""
335+
336+
def test_convert_uploaded_file_compression_disabled(self, temp_dir, test_audio_file):
337+
"""Test convert_uploaded_file with compression explicitly disabled"""
338+
filename = "test_audio.wav"
339+
test_audio_file(filename)
340+
341+
with patch('roundware.lib.convertaudio.compress_audio_file') as mock_compress:
342+
with patch('roundware.lib.convertaudio.normalize_audio_file') as mock_normalize:
343+
with patch('roundware.lib.convertaudio.convert_audio_file') as mock_convert:
344+
mock_convert.return_value = None
345+
346+
result = convert_uploaded_file(filename, enable_compression=False)
347+
348+
# Compression should NOT be called
349+
mock_compress.assert_not_called()
350+
351+
# Normalization should still be called
352+
mock_normalize.assert_called_once()
353+
354+
# Conversion should still be called
355+
assert mock_convert.call_count == 2 # m4a and mp3
356+
357+
assert result == "test_audio.mp3"
358+
359+
def test_convert_uploaded_file_compression_enabled(self, temp_dir, test_audio_file):
360+
"""Test convert_uploaded_file with compression explicitly enabled"""
361+
filename = "test_audio.wav"
362+
test_audio_file(filename)
363+
364+
with patch('roundware.lib.convertaudio.compress_audio_file') as mock_compress:
365+
with patch('roundware.lib.convertaudio.normalize_audio_file') as mock_normalize:
366+
with patch('roundware.lib.convertaudio.convert_audio_file') as mock_convert:
367+
mock_convert.return_value = None
368+
369+
result = convert_uploaded_file(filename, enable_compression=True)
370+
371+
# Compression should be called
372+
mock_compress.assert_called_once()
373+
374+
# Normalization should still be called
375+
mock_normalize.assert_called_once()
376+
377+
# Conversion should still be called
378+
assert mock_convert.call_count == 2 # m4a and mp3
379+
380+
assert result == "test_audio.mp3"
381+
382+
def test_convert_uploaded_file_default_parameter(self, temp_dir, test_audio_file):
383+
"""Test convert_uploaded_file with default parameter (uses settings)"""
384+
filename = "test_audio.wav"
385+
test_audio_file(filename)
386+
387+
with patch('roundware.lib.convertaudio.compress_audio_file') as mock_compress:
388+
with patch('roundware.lib.convertaudio.normalize_audio_file') as mock_normalize:
389+
with patch('roundware.lib.convertaudio.convert_audio_file') as mock_convert:
390+
mock_convert.return_value = None
391+
392+
# Test with settings.AUDIO_COMPRESSION_ENABLED = True
393+
with override_settings(AUDIO_COMPRESSION_ENABLED=True):
394+
result = convert_uploaded_file(filename) # No compression parameter
395+
396+
# Compression should be called (uses settings)
397+
mock_compress.assert_called_once()
398+
399+
# Normalization should still be called
400+
mock_normalize.assert_called_once()
401+
402+
assert result == "test_audio.mp3"
403+
404+
def test_speakers_api_compression_parameter_true(self, temp_dir):
405+
"""Test speakers API with audio_compression=true parameter"""
406+
from roundware.lib.api import save_speaker_from_request
407+
408+
# Create a mock request with audio_compression=true
409+
mock_request = MagicMock()
410+
mock_file = MagicMock()
411+
mock_file.name = "test_audio.wav"
412+
mock_file.file.read.return_value = b"fake audio content"
413+
mock_request.FILES = {'file': mock_file}
414+
mock_request.data = {'project': 1, 'audio_compression': True}
415+
mock_request.get_host.return_value = 'localhost:8000'
416+
417+
with patch('roundware.lib.api.convertaudio.convert_uploaded_file') as mock_convert:
418+
mock_convert.return_value = "test_audio.mp3"
419+
420+
result = save_speaker_from_request(mock_request)
421+
422+
# Should call convert_uploaded_file with enable_compression=True
423+
# The filename will include a timestamp, so we check the call args
424+
call_args = mock_convert.call_args
425+
assert call_args[0][0].startswith("speaker-project1-test_audio-") # filename
426+
assert call_args[0][1] is True # enable_compression
427+
428+
# Should return the expected URL
429+
assert ".mp3" in result
430+
431+
def test_speakers_api_compression_parameter_false(self, temp_dir):
432+
"""Test speakers API with audio_compression=false parameter"""
433+
from roundware.lib.api import save_speaker_from_request
434+
435+
# Create a mock request with audio_compression=false
436+
mock_request = MagicMock()
437+
mock_file = MagicMock()
438+
mock_file.name = "test_audio.wav"
439+
mock_file.file.read.return_value = b"fake audio content"
440+
mock_request.FILES = {'file': mock_file}
441+
mock_request.data = {'project': 1, 'audio_compression': False}
442+
mock_request.get_host.return_value = 'localhost:8000'
443+
444+
with patch('roundware.lib.api.convertaudio.convert_uploaded_file') as mock_convert:
445+
mock_convert.return_value = "test_audio.mp3"
446+
447+
result = save_speaker_from_request(mock_request)
448+
449+
# Should call convert_uploaded_file with enable_compression=False
450+
# The filename will include a timestamp, so we check the call args
451+
call_args = mock_convert.call_args
452+
assert call_args[0][0].startswith("speaker-project1-test_audio-") # filename
453+
assert call_args[0][1] is False # enable_compression
454+
455+
# Should return the expected URL
456+
assert ".mp3" in result
457+
458+
def test_speakers_api_compression_parameter_default(self, temp_dir):
459+
"""Test speakers API without audio_compression parameter (defaults to False)"""
460+
from roundware.lib.api import save_speaker_from_request
461+
462+
# Create a mock request without audio_compression parameter
463+
mock_request = MagicMock()
464+
mock_file = MagicMock()
465+
mock_file.name = "test_audio.wav"
466+
mock_file.file.read.return_value = b"fake audio content"
467+
mock_request.FILES = {'file': mock_file}
468+
mock_request.data = {'project': 1} # No audio_compression parameter
469+
mock_request.get_host.return_value = 'localhost:8000'
470+
471+
with patch('roundware.lib.api.convertaudio.convert_uploaded_file') as mock_convert:
472+
mock_convert.return_value = "test_audio.mp3"
473+
474+
result = save_speaker_from_request(mock_request)
475+
476+
# Should call convert_uploaded_file with enable_compression=False (default)
477+
# The filename will include a timestamp, so we check the call args
478+
call_args = mock_convert.call_args
479+
assert call_args[0][0].startswith("speaker-project1-test_audio-") # filename
480+
assert call_args[0][1] is False # enable_compression (default)
481+
482+
# Should return the expected URL
483+
assert ".mp3" in result
484+
485+
def test_speakers_api_compression_parameter_string_values(self, temp_dir):
486+
"""Test speakers API with string values for audio_compression parameter"""
487+
from roundware.lib.api import save_speaker_from_request
488+
489+
# Test with "true" string
490+
mock_request = MagicMock()
491+
mock_file = MagicMock()
492+
mock_file.name = "test_audio.wav"
493+
mock_file.file.read.return_value = b"fake audio content"
494+
mock_request.FILES = {'file': mock_file}
495+
mock_request.data = {'project': 1, 'audio_compression': 'true'}
496+
mock_request.get_host.return_value = 'localhost:8000'
497+
498+
with patch('roundware.lib.api.convertaudio.convert_uploaded_file') as mock_convert:
499+
mock_convert.return_value = "test_audio.mp3"
500+
501+
result = save_speaker_from_request(mock_request)
502+
503+
# Should call convert_uploaded_file with enable_compression=True
504+
# The filename will include a timestamp, so we check the call args
505+
call_args = mock_convert.call_args
506+
assert call_args[0][0].startswith("speaker-project1-test_audio-") # filename
507+
assert call_args[0][1] is True # enable_compression (string "true" converted)
508+
509+
# Should return the expected URL
510+
assert ".mp3" in result
511+
512+
def test_admin_upload_compression_disabled(self, temp_dir):
513+
"""Test that admin uploads have compression disabled by default"""
514+
from roundware.rw.file_utils import handle_speaker_audio_upload
515+
516+
# Create a mock speaker and request
517+
mock_speaker = MagicMock()
518+
mock_speaker.project.id = 1
519+
mock_speaker.code = 'admin_test'
520+
521+
mock_uploaded_file = MagicMock()
522+
mock_uploaded_file.name = 'admin_audio.wav'
523+
mock_uploaded_file.chunks.return_value = [b'fake audio content']
524+
525+
with patch('roundware.lib.convertaudio.convert_uploaded_file') as mock_convert:
526+
mock_convert.return_value = "admin_audio.mp3"
527+
528+
result = handle_speaker_audio_upload(mock_uploaded_file, mock_speaker)
529+
530+
# Should call convert_uploaded_file with enable_compression=False
531+
# The filename will include a timestamp, so we check the call args
532+
call_args = mock_convert.call_args
533+
if call_args and len(call_args[0]) >= 2:
534+
assert call_args[0][0].startswith("speaker-project1-admin_test-") # filename
535+
assert call_args[0][1] is False # enable_compression (admin default)
536+
else:
537+
# If called with keyword arguments, check those
538+
assert mock_convert.call_args[1]['enable_compression'] is False
539+
540+
# Should return the expected URI
541+
assert "admin_audio.mp3" in result

roundware/api2/views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,19 @@ def retrieve(self, request, pk=None):
12741274
def create(self, request):
12751275
"""
12761276
POST api/2/speakers/ - Create a new Speaker
1277+
1278+
Parameters:
1279+
- file: Audio file to upload (multipart/form-data)
1280+
- audio_compression: Boolean to enable/disable audio compression (default: false)
1281+
- project_id: Project ID
1282+
- code: Speaker code
1283+
- maxvolume: Maximum volume (0.0-1.0)
1284+
- minvolume: Minimum volume (0.0-1.0)
1285+
- uri: Audio URI (if not uploading file)
1286+
- backupuri: Backup audio URI
1287+
- shape: GeoJSON shape for speaker area
1288+
- attenuation_distance: Distance for audio attenuation
1289+
- activeyn: Whether speaker is active (boolean)
12771290
"""
12781291
if "project_id" in request.data:
12791292
request.data['project'] = request.data['project_id']

roundware/lib/api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,16 @@ def save_speaker_from_request(request):
773773

774774
# Do I need to delete the original file after copying to rwmedia/?
775775

776+
# Check for audio_compression parameter in request data
777+
# Default to False (disable compression) for better quality by default
778+
enable_compression = request.data.get('audio_compression', False)
779+
if isinstance(enable_compression, str):
780+
enable_compression = enable_compression.lower() in ('true', '1', 'yes', 'on')
781+
782+
logger.info("Audio compression setting: %s for file: %s", enable_compression, dest_filename)
783+
776784
# Make sure speaker audio is available in both mp3 and m4a (for iOS) to be comprehensive
777-
newfilename = convertaudio.convert_uploaded_file(dest_filename)
785+
newfilename = convertaudio.convert_uploaded_file(dest_filename, enable_compression)
778786
if not newfilename:
779787
raise RoundException("File not converted successfully: " + newfilename)
780788

roundware/lib/convertaudio.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,34 @@
1515

1616
# Converts the given file to both wav and mp3 and stores the files in the audio directory.
1717
# Handles files of various formats depending on the file extension.
18-
def convert_uploaded_file(filename):
18+
def convert_uploaded_file(filename, enable_compression=None):
19+
"""
20+
Convert uploaded audio file with optional compression control.
21+
22+
Args:
23+
filename: Name of the file to convert
24+
enable_compression: Boolean to override compression setting.
25+
If None, uses settings.AUDIO_COMPRESSION_ENABLED
26+
"""
1927
(filename_prefix, filename_extension) = os.path.splitext(filename)
2028
upload_dir = settings.MEDIA_ROOT
2129
filepath = os.path.join(upload_dir, filename)
2230
if not os.path.exists(filepath):
2331
raise RoundException(
2432
"Uploaded file not found: " + filepath)
2533
else:
26-
# Apply audio compression if enabled in settings (before normalization)
27-
if getattr(settings, 'AUDIO_COMPRESSION_ENABLED', True):
34+
# Determine if compression should be applied
35+
if enable_compression is None:
36+
compression_enabled = getattr(settings, 'AUDIO_COMPRESSION_ENABLED', True)
37+
else:
38+
compression_enabled = enable_compression
39+
40+
# Apply audio compression if enabled (before normalization)
41+
if compression_enabled:
2842
logger.info(f"Audio compression enabled - processing {filename}")
2943
compress_audio_file(upload_dir, filename_prefix, filename_extension)
3044
else:
31-
logger.debug(f"Audio compression disabled - skipping {filename}")
45+
logger.info(f"Audio compression disabled - skipping {filename}")
3246

3347
# Normalize audio if enabled in settings (after compression)
3448
if getattr(settings, 'AUDIO_NORMALIZATION_ENABLED', True):

roundware/rw/admin_upload.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ def clean_audio_upload(self):
7373
saved_path = default_storage.save(file_path, uploaded_file)
7474

7575
# Process the uploaded file (compression, normalization, format conversion)
76+
# Admin uploads default to NO compression for better quality
7677
try:
7778
from roundware.lib.convertaudio import convert_uploaded_file
78-
processed_filename = convert_uploaded_file(unique_filename)
79+
processed_filename = convert_uploaded_file(unique_filename, enable_compression=False)
7980
# Update saved_path to the processed version (MP3)
8081
saved_path = os.path.join('speaker_audio', processed_filename)
8182
except Exception as e:

roundware/rw/file_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ def handle_speaker_audio_upload(uploaded_file, speaker, request=None, project_id
5151
destination.write(chunk)
5252

5353
# Process the uploaded file (compression, normalization, format conversion)
54+
# Admin uploads default to NO compression for better quality
5455
try:
5556
from roundware.lib.convertaudio import convert_uploaded_file
56-
processed_filename = convert_uploaded_file(filename)
57+
processed_filename = convert_uploaded_file(filename, enable_compression=False)
5758
# Update filename to the processed version (MP3)
5859
filename = processed_filename
5960
file_path = os.path.join(settings.MEDIA_ROOT, filename)

0 commit comments

Comments
 (0)