Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ffmpeg resampling to handle non-standard sample rates #303

Merged
merged 6 commits into from
Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 7 additions & 23 deletions ld-decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -54,31 +54,15 @@
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':
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
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:
Expand Down
4 changes: 2 additions & 2 deletions lddecode_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down
60 changes: 54 additions & 6 deletions lddutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -162,10 +162,47 @@ 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, 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)]

return LoadFFmpeg(input_args=input_args, output_args=output_args)

elif filename.endswith('.lds'):
return load_packed_data_4_40
elif filename.endswith('.r30'):
return load_packed_data_3_32
elif filename.endswith('.r16'):
return load_unpacked_data_s16
elif filename.endswith('.r8'):
return load_unpacked_data_u8
elif filename.endswith('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)
Expand Down Expand Up @@ -283,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

Expand All @@ -296,6 +336,11 @@ def __init__(self):
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."""
Expand All @@ -313,8 +358,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)

Expand Down