From b2ac2543549a2b6dfab84962d7a05c45ff73ed86 Mon Sep 17 00:00:00 2001 From: Adam Sampson Date: Wed, 31 Jul 2019 18:09:16 +0100 Subject: [PATCH 1/6] Put parse_frequency's docstring in the right place. --- lddutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lddutils.py b/lddutils.py index 658b0f1ca..3a2807dfc 100644 --- a/lddutils.py +++ b/lddutils.py @@ -142,8 +142,8 @@ def downscale_field(data, lineinfo, outwidth=1820, lines=625, usewow=False): ("fscpal", (283.75 * 15625) + 25), ] -"""Parse an argument string, returning a float frequency in MHz.""" def parse_frequency(string): + """Parse an argument string, returning a float frequency in MHz.""" multiplier = 1.0e6 for suffix, mult in frequency_suffixes: if string.lower().endswith(suffix): From 7e4fba6a275109c512f10ed0a7eba37e51a22699 Mon Sep 17 00:00:00 2001 From: Adam Sampson Date: Wed, 31 Jul 2019 18:09:16 +0100 Subject: [PATCH 2/6] Remove some commented-out old input code. --- ld-decode.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ld-decode.py b/ld-decode.py index 5bff22a32..cb833af8b 100755 --- a/ld-decode.py +++ b/ld-decode.py @@ -54,15 +54,6 @@ print("ERROR: Can only be PAL or NTSC") exit(1) -# make sure we have at least two frames' worth of data (so we can be sure we will get at least one full frame) -#infile_size = os.path.getsize(filename) -#if (infile_size // bytes_per_frame - firstframe) < 2: - #print('Error: start frame is past end of file') - #exit(1) -#num_frames = req_frames if req_frames is not None else infile_size // bytes_per_frame - firstframe - -#fd = open(filename, 'rb') - if filename[-3:] == 'lds': loader = load_packed_data_4_40 elif filename[-3:] == 'r30': From a761147546f5188734ab461836c43c238cdd95f9 Mon Sep 17 00:00:00 2001 From: Adam Sampson Date: Wed, 31 Jul 2019 18:09:16 +0100 Subject: [PATCH 3/6] Move loader selection into a helper function in lddutils. --- ld-decode.py | 13 +------------ lddutils.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/ld-decode.py b/ld-decode.py index cb833af8b..90bfae428 100755 --- a/ld-decode.py +++ b/ld-decode.py @@ -54,18 +54,7 @@ print("ERROR: Can only be PAL or NTSC") exit(1) -if filename[-3:] == 'lds': - loader = load_packed_data_4_40 -elif filename[-3:] == 'r30': - loader = load_packed_data_3_32 -elif filename[-3:] == 'r16': - loader = load_unpacked_data_s16 -elif filename[-2:] == 'r8': - loader = load_unpacked_data_u8 -elif filename[-7:] == 'raw.oga': - loader = LoadFFmpeg() -else: - loader = load_packed_data_4_40 +loader = make_loader(filename) system = 'PAL' if args.pal else 'NTSC' diff --git a/lddutils.py b/lddutils.py index 3a2807dfc..52b49351b 100644 --- a/lddutils.py +++ b/lddutils.py @@ -166,6 +166,22 @@ def parse_frequency(string): This might probably need to become a full object once FLAC support is added. ''' +def make_loader(filename): + """Return an appropriate loader function for filename.""" + + if filename[-3:] == 'lds': + return load_packed_data_4_40 + elif filename[-3:] == 'r30': + return load_packed_data_3_32 + elif filename[-3:] == 'r16': + return load_unpacked_data_s16 + elif filename[-2:] == 'r8': + return load_unpacked_data_u8 + elif filename[-7:] == 'raw.oga': + return LoadFFmpeg() + else: + return load_packed_data_4_40 + def load_unpacked_data(infile, sample, readlen, sampletype): # this is run for unpacked data - 1 is for old cxadc data, 2 for 16bit DD infile.seek(sample * sampletype, 0) From a695aca92524cede6a2477ac9544a3dece84f5f6 Mon Sep 17 00:00:00 2001 From: Adam Sampson Date: Wed, 31 Jul 2019 18:09:16 +0100 Subject: [PATCH 4/6] Remove a to-do that's been done. --- lddutils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lddutils.py b/lddutils.py index 52b49351b..1d225070f 100644 --- a/lddutils.py +++ b/lddutils.py @@ -162,8 +162,6 @@ def parse_frequency(string): readlen: # of samples ``` Returns data if successful, or None or an upstream exception if not (including if not enough data is available) - -This might probably need to become a full object once FLAC support is added. ''' def make_loader(filename): From c0075adba9226bc37257ef5359fbe79f4f2f4020 Mon Sep 17 00:00:00 2001 From: Adam Sampson Date: Wed, 31 Jul 2019 18:09:16 +0100 Subject: [PATCH 5/6] Use ffmpeg resampling to handle non-standard sample rates. When -f is specified, load the input file through ffmpeg using the aresample filter to convert to 40MHz. This means the rest of ld-decode can run at its usual sample rate, which produces better-quality results and makes testing easier in the future. --- ld-decode.py | 10 +++++++--- lddecode_core.py | 4 ++-- lddutils.py | 49 ++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/ld-decode.py b/ld-decode.py index 90bfae428..2d904018f 100755 --- a/ld-decode.py +++ b/ld-decode.py @@ -37,7 +37,7 @@ parser.add_argument('-t', '--threads', metavar='threads', type=int, default=5, help='number of CPU threads to use') -parser.add_argument('-f', '--frequency', dest='inputfreq', metavar='FREQ', type=parse_frequency, default=40, help='RF sampling frequency (default is 40MHz)') +parser.add_argument('-f', '--frequency', dest='inputfreq', metavar='FREQ', type=parse_frequency, default=None, help='RF sampling frequency in source file (default is 40MHz)') parser.add_argument('--video_bpf_high', dest='vbpf_high', metavar='FREQ', type=parse_frequency, default=None, help='Video BPF high end frequency') parser.add_argument('--video_lpf', dest='vlpf', metavar='FREQ', type=parse_frequency, default=None, help='Video low-pass filter frequency') @@ -54,11 +54,15 @@ print("ERROR: Can only be PAL or NTSC") exit(1) -loader = make_loader(filename) +try: + loader = make_loader(filename, args.inputfreq) +except ValueError as e: + print(e) + exit(1) system = 'PAL' if args.pal else 'NTSC' -ldd = LDdecode(filename, outname, loader, inputfreq = args.inputfreq, analog_audio = not args.daa, digital_audio = not args.noefm, system=system, doDOD = not args.nodod, threads=args.threads) +ldd = LDdecode(filename, outname, loader, analog_audio = not args.daa, digital_audio = not args.noefm, system=system, doDOD = not args.nodod, threads=args.threads) ldd.roughseek(firstframe * 2) if system == 'NTSC' and not args.ntscj: diff --git a/lddecode_core.py b/lddecode_core.py index 2fb11c6f2..da527d362 100644 --- a/lddecode_core.py +++ b/lddecode_core.py @@ -2060,7 +2060,7 @@ def calcLine19Info(self, comb_field2 = None): class LDdecode: - def __init__(self, fname_in, fname_out, freader, inputfreq = 40, analog_audio = True, digital_audio = False, system = 'NTSC', doDOD = True, threads=4): + def __init__(self, fname_in, fname_out, freader, analog_audio = True, digital_audio = False, system = 'NTSC', doDOD = True, threads=4): self.demodcache = None self.infile = open(fname_in, 'rb') @@ -2104,7 +2104,7 @@ def __init__(self, fname_in, fname_out, freader, inputfreq = 40, analog_audio = self.fieldloc = 0 self.system = system - self.rf = RFDecode(inputfreq=inputfreq, system=system, decode_analog_audio=analog_audio, decode_digital_audio=digital_audio) + self.rf = RFDecode(system=system, decode_analog_audio=analog_audio, decode_digital_audio=digital_audio) if system == 'PAL': self.FieldClass = FieldPAL self.readlen = self.rf.linelen * 400 diff --git a/lddutils.py b/lddutils.py index 1d225070f..269b489a2 100644 --- a/lddutils.py +++ b/lddutils.py @@ -164,18 +164,41 @@ def parse_frequency(string): Returns data if successful, or None or an upstream exception if not (including if not enough data is available) ''' -def make_loader(filename): - """Return an appropriate loader function for filename.""" +def make_loader(filename, inputfreq=None): + """Return an appropriate loader function object for filename. + + If inputfreq is specified, it gives the sample rate in MHz of the source + file, and the loader will resample from that rate to 40 MHz. Any sample + rate specified by the source file's metadata will be ignored, as some + formats can't represent typical RF sample rates accurately.""" + + if inputfreq is not None: + # We're resampling, so we have to use ffmpeg. + + if filename.endswith('.r16'): + input_args = ['-f', 's16le'] + elif filename.endswith('.r8'): + input_args = ['-f', 'u8'] + elif filename.endswith('.lds') or filename.endswith('.r30'): + raise ValueError('File format not supported when resampling: ' + filename) + else: + # Assume ffmpeg will recognise this format itself. + input_args = [] + + # Use asetrate first to override the input file's sample rate. + output_args = ['-filter:a', 'asetrate=' + str(inputfreq * 1e6) + ',aresample=' + str(40e6)] - if filename[-3:] == 'lds': + return LoadFFmpeg(input_args=input_args, output_args=output_args) + + elif filename.endswith('.lds'): return load_packed_data_4_40 - elif filename[-3:] == 'r30': + elif filename.endswith('.r30'): return load_packed_data_3_32 - elif filename[-3:] == 'r16': + elif filename.endswith('.r16'): return load_unpacked_data_s16 - elif filename[-2:] == 'r8': + elif filename.endswith('.r8'): return load_unpacked_data_u8 - elif filename[-7:] == 'raw.oga': + elif filename.endswith('raw.oga'): return LoadFFmpeg() else: return load_packed_data_4_40 @@ -297,7 +320,10 @@ def load_packed_data_4_40(infile, sample, readlen): class LoadFFmpeg: """Load samples from a wide variety of formats using ffmpeg.""" - def __init__(self): + def __init__(self, input_args=[], output_args=[]): + self.input_args = input_args + self.output_args = output_args + # ffmpeg subprocess self.ffmpeg = None @@ -327,8 +353,11 @@ def __call__(self, infile, sample, readlen): readlen_bytes = readlen * 2 if self.ffmpeg is None: - command = ["ffmpeg", "-hide_banner", "-loglevel", "error", - "-i", "-", "-f", "s16le", "-c:a", "pcm_s16le", "-"] + command = ["ffmpeg", "-hide_banner", "-loglevel", "error"] + command += self.input_args + command += ["-i", "-"] + command += self.output_args + command += ["-c:a", "pcm_s16le", "-f", "s16le", "-"] self.ffmpeg = subprocess.Popen(command, stdin=infile, stdout=subprocess.PIPE) From c037471e17fb4a43a49361a5a5efe73ff73a7252 Mon Sep 17 00:00:00 2001 From: Adam Sampson Date: Wed, 31 Jul 2019 18:09:16 +0100 Subject: [PATCH 6/6] Kill the ffmpeg process in LoadFFmpeg's destructor. This avoids a broken-pipe warning from ffmpeg when using -l. (Strictly this will only work properly in CPython, since other implementations like PyPy don't guarantee the destructor will actually be run. Fixing this properly would mean changing the loader interface to have an explicit close() operation.) --- lddutils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lddutils.py b/lddutils.py index 269b489a2..bdfdd2a5d 100644 --- a/lddutils.py +++ b/lddutils.py @@ -336,6 +336,11 @@ def __init__(self, input_args=[], output_args=[]): self.rewind_size = 2 * 1024 * 1024 self.rewind_buf = b'' + def __del__(self): + if self.ffmpeg is not None: + self.ffmpeg.kill() + self.ffmpeg.wait() + def _read_data(self, count): """Read data as bytes from ffmpeg, append it to the rewind buffer, and return it. May return less than count bytes if EOF is reached."""