Last active
December 31, 2015 12:09
-
-
Save tdack/7984364 to your computer and use it in GitHub Desktop.
Commercial detection with silence for UK freeviewHD and Australian Freeview SD/HDSee http://www.mythtv.org/wiki/Commercial_detection_with_silence_for_UK_freeviewHD for detailsRunning:Assuming you use the same locations, your 'Advert-detection command' (mythtv-setup/General/Page 8) should be: /usr/local/bin/silence.py %JOBID% %VERBOSEMODE% --logl…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CC = g++ | |
CFLAGS = -c -Wall -std=c++0x | |
LIBPATH = -L/usr/lib | |
TARGETDIR = /usr/local/bin | |
.PHONY: clean install | |
all: silence | |
silence: silence.o | |
$(CC) silence.o -o $@ $(LIBPATH) -lsndfile | |
.cpp.o: | |
$(CC) $(CFLAGS) $< -o $@ | |
install: silence silence.py | |
install -p -t $(TARGETDIR) $^ | |
clean: | |
-rm -f silence *.o |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Based on mausc.c by Tinsel Phipps. | |
// v1.0 Roger Siddons | |
// v2.0 Roger Siddons: Flag clusters asap, fix segfaults, optional headers | |
// v3.0 Roger Siddons: Remove lib dependencies & commfree | |
// v4.0 Kill process argv[1] when idle for 30 seconds. | |
// v4.1 Fix averaging overflow | |
// v4.2 Unblock the alarm signal so the job actually finishes. | |
// Public domain. Requires libsndfile | |
// Detects commercial breaks using clusters of audio silences | |
#include <cstdlib> | |
#include <cmath> | |
#include <cerrno> | |
#include <climits> | |
#include <deque> | |
#include <sndfile.h> | |
#include <unistd.h> | |
#include <signal.h> | |
typedef unsigned frameNumber_t; | |
typedef unsigned frameCount_t; | |
// Output to python wrapper requires prefix to indicate level | |
#define DELIMITER "@" // must correlate with python wrapper | |
char prefixdebug[7] = "debug" DELIMITER; | |
char prefixinfo[6] = "info" DELIMITER; | |
char prefixerr[5] = "err" DELIMITER; | |
char prefixcut[5] = "cut" DELIMITER; | |
void error(const char* mesg, bool die = true) | |
{ | |
printf("%s%s\n", prefixerr, mesg); | |
if (die) | |
exit(1); | |
} | |
pid_t tail_pid = 0; | |
void watchdog(int sig) | |
{ | |
if (0 != tail_pid) | |
kill(tail_pid, SIGTERM); | |
} | |
namespace Arg | |
// Program argument management | |
{ | |
const float kvideoRate = 25.0; // sample rate in fps (maps time to frame count) | |
const frameCount_t krateInMins = kvideoRate * 60; // frames per min | |
unsigned useThreshold; // Audio level of silence | |
frameCount_t useMinQuiet; // Minimum length of a silence to register | |
unsigned useMinDetect; // Minimum number of silences that constitute an advert | |
frameCount_t useMinLength; // adverts must be at least this long | |
frameCount_t useMaxSep; // silences must be closer than this to be in the same cluster | |
frameCount_t usePad; // padding for each cut | |
void usage() | |
{ | |
error("Usage: silence <tail_pid> <threshold> <minquiet> <mindetect> <minlength> <maxsep> <pad>", false); | |
error("<tail_pid> : (int) Process ID to be killed after idle timeout.", false); | |
error("<threshold>: (float) silence threshold in dB.", false); | |
error("<minquiet> : (float) minimum time for silence detection in seconds.", false); | |
error("<mindetect>: (float) minimum number of silences to constitute an advert.", false); | |
error("<minlength>: (float) minimum length of advert break in seconds.", false); | |
error("<maxsep> : (float) maximum time between silences in an advert break in seconds.", false); | |
error("<pad> : (float) padding for each cut point in seconds.", false); | |
error("AU format audio is expected on stdin.", false); | |
error("Example: silence 4567 -75 0.1 5 60 90 1 < audio.au"); | |
} | |
void parse(int argc, char **argv) | |
// Parse args and convert to useable values (frames) | |
{ | |
if (8 != argc) | |
usage(); | |
float argThreshold; // db | |
float argMinQuiet; // secs | |
float argMinDetect; | |
float argMinLength; // secs | |
float argMaxSep; // secs | |
float argPad; // secs | |
/* Load options. */ | |
if (1 != sscanf(argv[1], "%d", &tail_pid)) | |
error("Could not parse tail_pid option into a number"); | |
if (1 != sscanf(argv[2], "%f", &argThreshold)) | |
error("Could not parse threshold option into a number"); | |
if (1 != sscanf(argv[3], "%f", &argMinQuiet)) | |
error("Could not parse minquiet option into a number"); | |
if (1 != sscanf(argv[4], "%f", &argMinDetect)) | |
error("Could not parse mindetect option into a number"); | |
if (1 != sscanf(argv[5], "%f", &argMinLength)) | |
error("Could not parse minlength option into a number"); | |
if (1 != sscanf(argv[6], "%f", &argMaxSep)) | |
error("Could not parse maxsep option into a number"); | |
if (1 != sscanf(argv[7], "%f", &argPad)) | |
error("Could not parse pad option into a number"); | |
/* Scale threshold to integer range that libsndfile will use. */ | |
useThreshold = rint(INT_MAX * pow(10, argThreshold / 20)); | |
/* Scale times to frames. */ | |
useMinQuiet = ceil(argMinQuiet * kvideoRate); | |
useMinDetect = (int)argMinDetect; | |
useMinLength = ceil(argMinLength * kvideoRate); | |
useMaxSep = rint(argMaxSep * kvideoRate + 0.5); | |
usePad = rint(argPad * kvideoRate + 0.5); | |
printf("%sThreshold=%.1f, MinQuiet=%.2f, MinDetect=%.1f, MinLength=%.1f, MaxSep=%.1f, Pad=%.2f\n", | |
prefixdebug, argThreshold, argMinQuiet, argMinDetect, argMinLength, argMaxSep, argPad); | |
printf("%sFrame rate is %.2f, Detecting silences below %d that last for at least %d frames\n", | |
prefixdebug, kvideoRate, useThreshold, useMinQuiet); | |
printf("%sClusters are composed of a minimum of %d silences closer than %d frames and must be\n", | |
prefixdebug, useMinDetect, useMaxSep); | |
printf("%slonger than %d frames in total. Cuts will be padded by %d frames\n", | |
prefixdebug, useMinLength, usePad); | |
printf("%s< preroll, > postroll, - advert, ? too few silences, # too short, = comm flagged\n", prefixdebug); | |
printf("%s Start - End Start - End Duration Interval Level/Count\n", prefixinfo); | |
printf("%s frame - frame (mmm:ss-mmm:ss) frame (mm:ss.s) frame (mmm:ss)\n", prefixinfo); | |
} | |
} | |
class Silence | |
// Defines a silence | |
{ | |
public: | |
enum state_t {progStart, detection, progEnd}; | |
static const char state_log[3]; | |
const state_t state; // type of silence | |
const frameNumber_t start; // frame of start | |
frameNumber_t end; // frame of end | |
frameCount_t length; // number of frames | |
frameCount_t interval; // frames between end of last silence & start of this one | |
double power; // average power level | |
Silence(frameNumber_t _start, double _power = 0, state_t _state = detection) | |
: state(_state), start(_start), end(_start), length(1), interval(0), power(_power) {} | |
void extend(frameNumber_t frame, double _power) | |
// Define end of the silence | |
{ | |
end = frame; | |
length = frame - start + 1; | |
// maintain running average power: = (oldpower * (newlength - 1) + newpower)/ newlength | |
power += (_power - power)/length; | |
} | |
}; | |
// c++0x doesn't allow initialisation within class | |
const char Silence::state_log[3] = {'<', ' ', '>'}; | |
class Cluster | |
// A cluster of silences | |
{ | |
private: | |
void setState() | |
{ | |
if (this->start->start == 1) | |
state = preroll; | |
else if (this->end->state == Silence::progEnd) | |
state = postroll; | |
else if (length < Arg::useMinLength) | |
state = tooshort; | |
else if (silenceCount < Arg::useMinDetect) | |
state = toofew; | |
else | |
state = advert; | |
} | |
public: | |
// tooshort..unset are transient states - they may be updated, preroll..postroll are final | |
enum state_t {tooshort, toofew, unset, preroll, advert, postroll}; | |
static const char state_log[6]; | |
static frameNumber_t completesAt; // frame where the most recent cluster will complete | |
state_t state; // type of cluster | |
const Silence* start; // first silence | |
Silence* end; // last silence | |
frameNumber_t padStart, padEnd; // padded cluster start/end frames | |
unsigned silenceCount; // number of silences | |
frameCount_t length; // number of frames | |
frameCount_t interval; // frames between end of last cluster and start of this one | |
Cluster(Silence* s) : state(unset), start(s), end(s), silenceCount(1), length(s->length), interval(0) | |
{ | |
completesAt = end->end + Arg::useMaxSep; // finish cluster <maxsep> beyond silence end | |
setState(); | |
// pad everything except pre-rolls | |
padStart = (state == preroll ? 1 : start->start + Arg::usePad); | |
} | |
void extend(Silence* _end) | |
// Define end of a cluster | |
{ | |
end = _end; | |
silenceCount++; | |
length = end->end - start->start + 1; | |
completesAt = end->end + Arg::useMaxSep; // finish cluster <maxsep> beyond silence end | |
setState(); | |
// pad everything except post-rolls | |
padEnd = end->end - (state == postroll ? 0 : Arg::usePad); | |
} | |
}; | |
// c++0x doesn't allow initialisation within class | |
const char Cluster::state_log[6] = {'#', '?', '.', '<', '-', '>'}; | |
frameNumber_t Cluster::completesAt = 0; | |
class ClusterList | |
// Manages a list of detected silences and a list of assigned clusters | |
{ | |
protected: | |
// list of detected silences | |
std::deque<Silence*> silence; | |
// list of deduced clusters of the silences | |
std::deque<Cluster*> cluster; | |
public: | |
Silence* insertStartSilence() | |
// Inserts a fake silence at the front of the silence list | |
{ | |
// create a single frame silence at frame 1 and insert it at front | |
Silence* ref = new Silence(1, 0, Silence::progStart); | |
silence.push_front(ref); | |
return ref; | |
} | |
void addSilence(Silence* newSilence) | |
// Adds a silence detection to the end of the silence list | |
{ | |
// set interval between this & previous silence/prog start | |
newSilence->interval = newSilence->start | |
- (silence.empty() ? 1 : silence.back()->end - 1); | |
// store silence | |
silence.push_back(newSilence); | |
} | |
void addCluster(Cluster* newCluster) | |
// Adds a cluster to end of the cluster list | |
{ | |
// set interval between new cluster & previous one/prog start | |
newCluster->interval = newCluster->start->start | |
- (cluster.empty() ? 1 : cluster.back()->end->end - 1); | |
// store cluster | |
cluster.push_back(newCluster); | |
} | |
}; | |
Silence* currentSilence; // the silence currently being detected/built | |
Cluster* currentCluster; // the cluster currently being built | |
ClusterList* clist; // List of completed silences & clusters | |
void report(const char* err, | |
const char type, | |
const char* msg1, | |
const frameNumber_t start, | |
const frameNumber_t end, | |
const frameNumber_t interval, | |
const int power) | |
// Logs silences/clusters/cuts in a standard format | |
{ | |
frameCount_t duration = end - start + 1; | |
printf("%s%c %7s %6d-%6d (%3d:%02ld-%3d:%02ld), %4d (%2d:%04.1f), %5d (%3d:%02ld), [%7d]\n", | |
err, type, msg1, start, end, | |
(start+13) / Arg::krateInMins, lrint(start / Arg::kvideoRate) % 60, | |
(end+13) / Arg::krateInMins, lrint(end / Arg::kvideoRate) % 60, | |
duration, (duration+1) / Arg::krateInMins, fmod(duration / Arg::kvideoRate, 60), | |
interval, (interval+13) / Arg::krateInMins, lrint(interval / Arg::kvideoRate) % 60, power); | |
} | |
void processSilence() | |
// Process a silence detection | |
{ | |
// ignore detections that are too short | |
if (currentSilence->state == Silence::detection && currentSilence->length < Arg::useMinQuiet) | |
{ | |
// throw it away | |
delete currentSilence; | |
currentSilence = NULL; | |
} | |
else | |
{ | |
// record new silence | |
clist->addSilence(currentSilence); | |
// assign it to a cluster | |
if (currentCluster) | |
{ | |
// add to existing cluster | |
currentCluster->extend(currentSilence); | |
} | |
else if (currentSilence->interval <= Arg::useMaxSep) // only possible for very first silence | |
{ | |
// First silence is close to prog start so extend cluster to the start | |
// by inserting a fake silence at prog start and starting the cluster there | |
currentCluster = new Cluster(clist->insertStartSilence()); | |
currentCluster->extend(currentSilence); | |
} | |
else | |
{ | |
// this silence is the start of a new cluster | |
currentCluster = new Cluster(currentSilence); | |
} | |
report(prefixdebug, currentSilence->state_log[currentSilence->state], "Silence", | |
currentSilence->start, currentSilence->end, | |
currentSilence->interval, currentSilence->power); | |
// silence is now owned by the list, start looking for next | |
currentSilence = NULL; | |
} | |
} | |
void processCluster() | |
// Process a completed cluster | |
{ | |
// record new cluster | |
clist->addCluster(currentCluster); | |
report(prefixinfo, currentCluster->state_log[currentCluster->state], "Cluster", | |
currentCluster->start->start, currentCluster->end->end, | |
currentCluster->interval, currentCluster->silenceCount); | |
// only flag clusters at final state | |
if (currentCluster->state > Cluster::unset) | |
report(prefixcut, '=', "Cut", currentCluster->padStart, currentCluster->padEnd, 0, 0); | |
// cluster is now owned by the list, start looking for next | |
currentCluster = NULL; | |
} | |
int main(int argc, char **argv) | |
// Detect silences and allocate to clusters | |
{ | |
// Remove logging prefixes if writing to terminal | |
if (isatty(1)) | |
prefixcut[0] = prefixinfo[0] = prefixdebug[0] = prefixerr[0] = '\0'; | |
// flush output buffer after every line | |
setvbuf(stdout, NULL, _IOLBF, 0); | |
Arg::parse(argc, argv); | |
/* Check the input is an audiofile. */ | |
SF_INFO metadata; | |
SNDFILE* input = sf_open_fd(STDIN_FILENO, SFM_READ, &metadata, SF_FALSE); | |
if (NULL == input) { | |
error("libsndfile error:", false); | |
error(sf_strerror(NULL)); | |
} | |
/* Allocate data buffer to contain audio data from one video frame. */ | |
const size_t frameSamples = metadata.channels * metadata.samplerate / Arg::kvideoRate; | |
int* samples = (int*)malloc(frameSamples * sizeof(int)); | |
if (NULL == samples) | |
error("Couldn't allocate memory"); | |
// create silence/cluster list | |
clist = new ClusterList(); | |
// Kill head of pipeline if timeout happens. | |
signal(SIGALRM, watchdog); | |
sigset_t intmask; | |
sigemptyset(&intmask); | |
sigaddset(&intmask, SIGALRM); | |
sigprocmask(SIG_UNBLOCK, &intmask, NULL); | |
alarm(30); | |
// Process the input one frame at a time and process cuts along the way. | |
frameNumber_t frames = 0; | |
while (frameSamples == static_cast<size_t>(sf_read_int(input, samples, frameSamples))) | |
{ | |
alarm(30); | |
frames++; | |
// determine average audio level in this frame | |
unsigned long long avgabs = 0; | |
for (unsigned i = 0; i < frameSamples; i++) | |
avgabs += abs(samples[i]); | |
avgabs = avgabs / frameSamples; | |
// check for a silence | |
if (avgabs < Arg::useThreshold) | |
{ | |
if (currentSilence) | |
{ | |
// extend current silence | |
currentSilence->extend(frames, avgabs); | |
} | |
else // transition to silence | |
{ | |
// start a new silence | |
currentSilence = new Silence(frames, avgabs); | |
} | |
} | |
else if (currentSilence) // transition out of silence | |
{ | |
processSilence(); | |
} | |
// in noise: check for cluster completion | |
else if (currentCluster && frames > currentCluster->completesAt) | |
{ | |
processCluster(); | |
} | |
} | |
// Complete any current silence (prog may have finished in silence) | |
if (currentSilence) | |
{ | |
processSilence(); | |
} | |
// extend any cluster close to prog end | |
if (currentCluster && frames <= currentCluster->completesAt) | |
{ | |
// generate a silence at prog end and extend cluster to it | |
currentSilence = new Silence(frames, 0, Silence::progEnd); | |
processSilence(); | |
} | |
// Complete any final cluster | |
if (currentCluster) | |
{ | |
processCluster(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# Build a skiplist from silence in the audio track. | |
# v1.0 Roger Siddons | |
# v2.0 Fix progid for job/player messages | |
# v3.0 Send player messages via Python | |
# v3.1 Fix commflag status, pad preset. Improve style & make Python 3 compatible | |
# v4.0 silence.cpp will kill the head of the pipeline (tail) when recording finished | |
# v4.1 Use unicode for foreign chars | |
# v4.2 Prevent BE writeStringList errors | |
# v5.0 Improve exception handling/logging. Fix player messages (0.26+ only) | |
import MythTV | |
import os | |
import subprocess | |
import argparse | |
import collections | |
import re | |
import sys | |
kExe_Silence = '/usr/local/bin/silence' | |
kUpmix_Channels = '6' # Change this to 2 if you never have surround sound in your recordings. | |
class MYLOG(MythTV.MythLog): | |
"A specialised logger" | |
def __init__(self, db): | |
"Initialise logging" | |
MythTV.MythLog.__init__(self, '', db) | |
def log(self, msg, level = MythTV.MythLog.INFO): | |
"Log message" | |
# prepend string to msg so that rsyslog routes it to mythcommflag.log logfile | |
MythTV.MythLog.log(self, MythTV.MythLog.COMMFLAG, level, 'mythcommflag: ' + msg.rstrip('\n')) | |
class PRESET: | |
"Manages the presets (parameters passed to the detection algorithm)" | |
# define arg ordering and default values | |
argname = ['thresh', 'minquiet', 'mindetect', 'minbreak', 'maxsep', 'pad'] | |
argval = [ -75, 0.16, 6, 120, 120, 0.48] | |
# dictionary holds value for each arg | |
argdict = collections.OrderedDict(list(zip(argname, argval))) | |
def _validate(self, k, v): | |
"Converts arg input from string to float or None if invalid/not supplied" | |
if v is None or v == '': | |
return k, None | |
try: | |
return k, float(v) | |
except ValueError: | |
self.logger.log('Preset ' + k + ' (' + str(v) + ') is invalid - will use default', | |
MYLOG.ERR) | |
return k, None | |
def __init__(self, _logger): | |
"Initialise preset manager" | |
self.logger = _logger | |
def getFromArg(self, line): | |
"Parses preset values from command-line string" | |
self.logger.log('Parsing presets from "' + line + '"', MYLOG.DEBUG) | |
if line: # ignore empty string | |
vals = [i.strip() for i in line.split(',')] # split individual params | |
# convert supplied values to float & match to appropriate arg name | |
validargs = list(map(self._validate, self.argname, vals[0:len(self.argname)])) | |
# remove missing/invalid values from list & replace default values with the rest | |
self.argdict.update(v for v in validargs if v[1] is not None) | |
def getFromFile(self, filename, title, callsign): | |
"Gets preset values from a file" | |
self.logger.log('Using preset file "' + filename + '"', MYLOG.DEBUG) | |
try: | |
with open(filename) as presets: | |
for rawline in presets: | |
line = rawline.strip() | |
if line and (not line.startswith('#')): # ignore empty & comment lines | |
vals = [i.strip() for i in line.split(',')] # split individual params | |
# match preset name to recording title or channel | |
pattern = re.compile(vals[0], re.IGNORECASE) | |
if pattern.match(title) or pattern.match(callsign): | |
self.logger.log('Using preset "' + line.strip() + '"') | |
# convert supplied values to float & match to appropriate arg name | |
validargs = list(map(self._validate, self.argname, | |
vals[1:1 + len(self.argname)])) | |
# remove missing/invalid values from list & | |
# replace default values with the rest | |
self.argdict.update(v for v in validargs if v[1] is not None) | |
break | |
else: | |
self.logger.log('No preset found for "' + title.encode('utf-8') + '" or "' + callsign.encode('utf-8') + '"') | |
except IOError: | |
self.logger.log('Presets file "' + filename + '" not found', MYLOG.ERR) | |
return self.argdict | |
def getValues(self): | |
"Returns params as a list of strings" | |
return [str(i) for i in list(self.argdict.values())] | |
def main(): | |
"Commflag a recording" | |
try: | |
# define options | |
parser = argparse.ArgumentParser(description='Commflagger') | |
parser.add_argument('--preset', help='Specify values as "Threshold, MinQuiet, MinDetect, MinLength, MaxSep, Pad"') | |
parser.add_argument('--presetfile', help='Specify file containing preset values') | |
parser.add_argument('--chanid', type=int, help='Use chanid for manual operation') | |
parser.add_argument('--starttime', help='Use starttime for manual operation') | |
parser.add_argument('--dump', action="store_true", help='Generate stack trace of exception') | |
parser.add_argument('jobid', nargs='?', help='Myth job id') | |
# must set up log attributes before Db locks them | |
MYLOG.loadArgParse(parser) | |
MYLOG._setmask(MYLOG.COMMFLAG) | |
# parse options | |
args = parser.parse_args() | |
# connect to backend | |
db = MythTV.MythDB() | |
logger = MYLOG(db) | |
be = MythTV.BECache(db=db) | |
logger.log('') # separate jobs in logfile | |
if args.jobid: | |
logger.log('Starting job %s'%args.jobid, MYLOG.INFO) | |
job = MythTV.Job(args.jobid, db) | |
chanid = job.chanid | |
starttime = job.starttime | |
elif args.chanid and args.starttime: | |
job = None | |
chanid = args.chanid | |
try: | |
# only 0.26+ | |
starttime = MythTV.datetime.duck(args.starttime) | |
except AttributeError: | |
starttime = args.starttimeaction="store_true" | |
else: | |
logger.log('Both --chanid and -starttime must be specified', MYLOG.ERR) | |
sys.exit(1) | |
# mythplayer update message uses a 'chanid_utcTimeAsISODate' format to identify recording | |
try: | |
# only 0.26+ | |
utc = starttime.asnaiveutc() | |
except AttributeError: | |
utc = starttime | |
progId = '%d_%s'%(chanid, str(utc).replace(' ', 'T')) | |
# get recording | |
logger.log('Seeking chanid %s, starttime %s' %(chanid, starttime), MYLOG.INFO) | |
rec = MythTV.Recorded((chanid, starttime), db) | |
channel = MythTV.Channel(chanid, db) | |
logger.log('Processing: ' + channel.callsign.encode('utf-8') + ', ' + str(rec.starttime) | |
+ ', "' + rec.title.encode('utf-8') + ' - ' + rec.subtitle.encode('utf-8')+ '"') | |
sg = MythTV.findfile(rec.basename, rec.storagegroup, db) | |
if sg is None: | |
logger.log("Can't access file %s from %s"%(rec.basename, rec.storagegroup), MYLOG.ERR) | |
try: | |
job.update({'status': job.ERRORED, 'comment': "Couldn't access file"}) | |
except AttributeError : pass | |
sys.exit(1) | |
# create params with default values | |
param = PRESET(logger) | |
# read any supplied presets | |
if args.preset: | |
param.getFromArg(args.preset) | |
elif args.presetfile: # use preset file | |
param.getFromFile(args.presetfile, rec.title, channel.callsign) | |
# Pipe file through ffmpeg to extract uncompressed audio stream. Keep going till recording is finished. | |
infile = os.path.join(sg.dirname, rec.basename) | |
p1 = subprocess.Popen(["tail", "--follow", "--bytes=+1", infile], stdout=subprocess.PIPE) | |
p2 = subprocess.Popen(["mythffmpeg", "-loglevel", "quiet", "-i", "pipe:0", | |
"-f", "au", "-ac", kUpmix_Channels, "-"], | |
stdin=p1.stdout, stdout=subprocess.PIPE) | |
# Pipe audio stream to C++ silence which will spit out formatted log lines | |
p3 = subprocess.Popen([kExe_Silence, "%d" % p1.pid] + param.getValues(), stdin=p2.stdout, | |
stdout=subprocess.PIPE) | |
# Purge any existing skip list and flag as in-progress | |
rec.commflagged = 2 | |
rec.markup.clean() | |
rec.update() | |
# Process log output from C++ silence | |
breaks = 0 | |
level = {'info': MYLOG.INFO, 'debug': MYLOG.DEBUG, 'err': MYLOG.ERR} | |
while True: | |
line = p3.stdout.readline() | |
if line: | |
flag, info = line.split('@', 1) | |
if flag == 'cut': | |
# extract numbers from log line | |
numbers = re.findall('\d+', info) | |
logger.log(info) | |
# mark advert in database | |
rec.markup.append(int(numbers[0]), rec.markup.MARK_COMM_START, None) | |
rec.markup.append(int(numbers[1]), rec.markup.MARK_COMM_END, None) | |
rec.update() | |
breaks += 1 | |
# send new advert skiplist to MythPlayers | |
skiplist = ['%d:%d,%d:%d'%(x, rec.markup.MARK_COMM_START, y, rec.markup.MARK_COMM_END) | |
for x, y in rec.markup.getskiplist()] | |
mesg = 'COMMFLAG_UPDATE %s %s'%(progId, ','.join(skiplist)) | |
# logger.log(' Sending %s'%mesg, MYLOG.DEBUG) | |
result = be.backendCommand("MESSAGE[]:[]" + mesg) | |
if result != 'OK': | |
logger.log('Sending update message to backend failed, response = %s, message = %s'% (result, mesg), MYLOG.ERR) | |
elif flag in level: | |
logger.log(info, level.get(flag)) | |
else: # unexpected prefix | |
# use warning for unexpected log levels | |
logger.log(flag, MYLOG.WARNING) | |
else: | |
break | |
# Signal comflagging has finished | |
rec.commflagged = 1 | |
rec.update() | |
logger.log('Detected %s adverts.' % breaks) | |
try: | |
job.update({'status': 272, 'comment': 'Detected %s adverts.' % breaks}) | |
except AttributeError : pass | |
# Finishing too quickly can cause writeStringList/socket errors in the BE. (pre-0.28 only?) | |
# A short delay prevents this | |
import time | |
time.sleep(1) | |
except Exception as e: | |
# get exception before we generate another | |
import traceback | |
exc_type, exc_value, frame = sys.exc_info() | |
# get stacktrace as a list | |
stack = traceback.format_exception(exc_type, exc_value, frame) | |
# set status | |
status = 'Failed due to: "%s"'%e | |
try: | |
logger.log(status, MYLOG.ERR) | |
except : pass | |
try: | |
job.update({'status': job.ERRORED, 'comment': 'Failed.'}) | |
except : pass | |
# populate stack trace with vars | |
try: | |
if args.dump: | |
# insert the frame local vars after each frame trace | |
# i is the line index following the frame trace; 0 is the trace mesg, 1 is the first code line | |
i = 2 | |
while frame is not None: | |
# format local vars | |
vars = [] | |
for name, var in frame.tb_frame.f_locals.iteritems(): | |
try: | |
text = '%s' % var | |
# truncate vars that are too long | |
if len(text) > 1000: | |
text = text[:1000] + '...' | |
except Exception as e: # some var defs may be incomplete | |
text = '<Unknown due to: %s>'%e | |
vars.append('@ %s = %s'%(name, text)) | |
# insert local stack contents after code trace line | |
stack.insert(i, '\n'.join(['@-------------'] + vars + ['@-------------\n'])) | |
# advance over our insertion & the next code trace line | |
i += 2 | |
frame = frame.tb_next | |
logger.log('\n'.join(stack), MYLOG.ERR) | |
except : pass | |
sys.exit(1) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# presets for silence.py | |
# use comma separated values: defaults are used for absent values | |
# For titles/callsign the name is a python regular expressions, case is ignored. | |
# Re Metachars are # . ^ $ * + ? { } [ ] \ | ( ) | |
# If a title contains one of these, then escape it (using \) or replace it with full stop | |
# Names are matched to the START of a title/callsign so "e4" also matches "e4+1" | |
# First name match is used so put specific presets (ie. programmes) before general ones (channels) | |
# | |
# title/callsign, threshold, minquiet, mindetect, minbreak, maxsep, padding | |
# defaults -75, 0.16, 6, 120, 120, 0.48, | |
# | |
# Defaults for Australian Freeview channels. | |
NINE DIGITAL, -73, 0.16, 6, 150, 60, 0.48, | |
GEM, -73, 0.16, 5, 120, 60, 0.48, | |
GO!, -73, 0.16, 5, 120, 60, 0.48, | |
7 Digital, -75, 0.16, 5, 150, 60, 0.48, | |
#ABC1 - No ads, have not bothered attempting to configure for preroll or postroll. | |
#ABC News 24 - No ads | |
#ABC2 - ABC4 - No ads, have not bothered attempting to configure for preroll or postroll. | |
#ABC3 - No ads, have not bothered attempting to configure for preroll or postroll. | |
#7mate – defaults working okay with limited testing | |
#7TWO – defaults working okay so far | |
#TEN Digital – defaults working okay so far | |
#ELEVEN – defaults working okay so far | |
#ONE – defaults working okay so far | |
#SBS ONE – defaults working okay with limited testing | |
#SBS TWO – defaults working okay with limited testing | |
#SBS HD – defaults working okay with limited testing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# presets for silence.py | |
# use comma separated values: defaults are used for absent values | |
# For titles/callsign the name is a python regular expressions, case is ignored. | |
# Re Metachars are # . ^ $ * + ? { } [ ] \ | ( ) | |
# If a title contains one of these, then escape it (using \) or replace it with full stop | |
# Names are matched to the START of a title/callsign so "e4" also matches "e4+1" | |
# First name match is used so put specific presets (ie. programmes) before general ones (channels) | |
# | |
# title/callsign, threshold, minquiet, mindetect, minbreak, maxsep, padding | |
# defaults -75, 0.16, 6, 120, 120, 0.48, | |
# | |
frasier, , 0.28, , , 91, , long pauses in prog | |
channel 4 news, , 1.00, 1, 55, , 0, short advert, many silences | |
milkshake, , 0.48, 8, 60, 61, , ignore short silences in animation/links | |
rude tube, , 0.32, , 180, 61, , ignore short silences in links | |
channel 4, , 0.24, | |
more 4, , 0.24, | |
dave, -71, , , , , , loud silences | |
quest, , , , , 55, , short silences, long breaks, short ads | |
channel 5, , 0.24, 2, , 300, , cut news out of films | |
itv, , , , , , 1.0, long pad for films | |
film 4, , , , , , 1.0, long pad for films | |
bbc, , 0.48, 1, 20, 360, 0, pre/post-roll | |
cbeebies, , 0.48, 1, 20, 360, 0, pre/post-roll | |
cbbc, , 0.48, 1, 20, 360, 0, pre/post-roll |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment