Skip to content

Commit

Permalink
Silence detection
Browse files Browse the repository at this point in the history
  • Loading branch information
dtcooper committed Aug 8, 2024
1 parent b2bf8f3 commit f3500b8
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 18 deletions.
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Changes for 2024 based on real world usage in 2023 and feedback
- [x] Single app client lock (ie only ONE client per username/password)'
- [ ] Way to parse filename into rotator, start/end date
- [ ] Submit form built into Tomato?
- [ ] Silence detection REJECTs audio assets in backend (if there's more than 2 seconds?) (behind FEATURE flag)
- [x] Silence detection REJECTs audio assets in backend (if there's more than 2 seconds?) (behind FEATURE flag)
- [x] Export all audio assets as zip
- [x] Import as well (have to be careful with different `protocol.json:protocol_version`)
- [ ] Add configurable silence between ads. Crossfade, with fade points? Fancy!
Expand Down
33 changes: 33 additions & 0 deletions server/tomato/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import logging
import math
import re
import shlex
import subprocess

Expand All @@ -11,6 +12,7 @@

logger = logging.getLogger(__name__)
FFProbe = namedtuple("FFProbe", ("format", "duration", "title"))
SILENCEDETECT_RE = re.compile(r"^\[silencedetect.+silence_duration: ([\d\.]+)", re.MULTILINE)


def run_command(args):
Expand Down Expand Up @@ -123,3 +125,34 @@ def ffmpeg_convert(infile, outfile):
trimmed_wav_file.unlink(missing_ok=True)

return cmd.returncode == 0


def silence_detect(infile):
if config.REJECT_SILENCE_LENGTH == 0:
return (False, 0)

cmd = run_command(
(
"ffmpeg",
"-y",
"-i",
infile,
"-hide_banner",
"-af",
f"silencedetect=n=-32dB:d={config.REJECT_SILENCE_LENGTH}",
"-f",
"null",
"-",
),
)
if cmd.returncode != 0:
logger.error(f"ffmpeg returned {cmd.returncode} while detecting silence in {infile}: {cmd.stderr}")
return (False, 0)

silences = SILENCEDETECT_RE.findall(cmd.stderr)
if silences:
max_silence = max(math.ceil(float(s)) for s in SILENCEDETECT_RE.findall(cmd.stderr))
logger.warning(f"{infile} has a silence of {max_silence} seconds in it!")
return (True, max_silence)
else:
return (False, 0)
44 changes: 28 additions & 16 deletions server/tomato/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from constance import config
from dirtyfields import DirtyFieldsMixin

from ..ffmpeg import silence_detect
from ..utils import listdir_recursive
from .base import (
FILE_MAX_LENGTH,
Expand Down Expand Up @@ -87,22 +88,33 @@ def serialize(self):

def full_clean(self, *args, **kwargs):
super().full_clean(*args, **kwargs)
if config.PREVENT_DUPLICATE_ASSETS and self.file and "file" in self.get_dirty_fields():
md5sum = self.generate_md5sum()
querysets = {
Asset: Asset.objects.filter(pre_process_md5sum=md5sum),
AssetAlternate: AssetAlternate.objects.filter(pre_process_md5sum=md5sum),
}
if self.id is not None:
querysets[self._meta.model] = querysets[self._meta.model].exclude(id=self.id)
if any(qs.exists() for qs in querysets.values()):
raise ValidationError({
"__all__": mark_safe(
"An audio asset already exists with this audio file. Rejecting duplicate. You can turn this"
" feature off with setting <code>PREVENT_DUPLICATES</code>."
),
"file": "A duplicate of this file already exists.",
})
if self.file and "file" in self.get_dirty_fields():
if config.PREVENT_DUPLICATE_ASSETS:
md5sum = self.generate_md5sum()
querysets = {
Asset: Asset.objects.filter(pre_process_md5sum=md5sum),
AssetAlternate: AssetAlternate.objects.filter(pre_process_md5sum=md5sum),
}
if self.id is not None:
querysets[self._meta.model] = querysets[self._meta.model].exclude(id=self.id)
if any(qs.exists() for qs in querysets.values()):
raise ValidationError({
"__all__": mark_safe(
"An audio asset already exists with this audio file. Rejecting duplicate. You can turn this"
" feature off with setting <code>PREVENT_DUPLICATES</code>."
),
"file": "A duplicate of this file already exists.",
})
if config.REJECT_SILENCE_LENGTH > 0:
has_silence, silence_duration = silence_detect(self.file.real_path)
if has_silence:
raise ValidationError({
"__all__": mark_safe(
f"This audio asset contains a silence of {silence_duration}s. Rejecting. You feature off"
" with setting <code>REJECT_SILENCE_LENGTH</code>."
),
"file": f"This asset contains a silence of {silence_duration} seconds.",
})

def save(self, dont_overwrite_original_filename=False, *args, **kwargs):
if not dont_overwrite_original_filename and "file" in self.get_dirty_fields():
Expand Down
16 changes: 15 additions & 1 deletion server/tomato/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ def validate_reset_times(values):
"validators": (validate_reset_times,),
},
),
"silence": (
"django.forms.IntegerField",
{
"widget": "django.forms.TextInput",
"min_value": 0,
"max_value": 60,
},
),
}

CONSTANCE_CONFIG = {
Expand Down Expand Up @@ -439,6 +447,11 @@ def validate_reset_times(values):
"WARN_ON_EMPTY_ROTATORS": (True, "Warn when a rotator is disabled or has no eligible assets to choose from."),
"RELOAD_PLAYLIST_AFTER_DATA_CHANGES": (False, "Reload all connected client playlists when a data change occurs."),
"ONE_CLIENT_LOGIN_PER_ACCOUNT": (False, "Only allow one desktop client to be connected per account."),
"REJECT_SILENCE_LENGTH": (
0,
mark_safe("Reject silence assets of this many seconds. <strong>Set to 0 disable</strong>."),
"silence",
),
}

CONSTANCE_CONFIG_FIELDSETS = OrderedDict((
Expand All @@ -458,6 +471,7 @@ def validate_reset_times(values):
"EXTRACT_METADATA_FROM_FILE",
"PREVENT_DUPLICATE_ASSETS",
"TRIM_SILENCE",
"REJECT_SILENCE_LENGTH",
"RELOAD_PLAYLIST_AFTER_DATA_CHANGES",
),
),
Expand All @@ -483,7 +497,7 @@ def validate_reset_times(values):
"from constance import config",
"from user_messages import api as user_messages_api",
"from tomato import constants",
"from tomato.ffmpeg import ffmpeg_convert, ffprobe",
"from tomato.ffmpeg import ffmpeg_convert, ffprobe, silence_detect",
"from tomato.models import export_data_as_zip, import_data_from_zip, serialize_for_api",
"from tomato.tasks import bulk_process_assets, process_asset, cleanup",
]

0 comments on commit f3500b8

Please sign in to comment.