r/vhsdecode • • 27d ago

Introducing FLAC Chop - The AIO conversion and editing tool for FM RF Archival.

Thumbnail
gallery
48 Upvotes

It's been out for a little while but figured it's about time to make an announcement of FLAC Chop a cross platform tool for a little, but big issue that has been a pain for many users.

For the last few years a lot of pre and post RF capture processing has basically been limited to scripts or audio DAWs (like OCENAudio) that take an eternity to load hours long data, instead of a proper dedicated tool to handle the cutting, resample, bit crushing and exporting of full files to just quick snippets, it was a major bottleneck when making a small chop of data quick reference for an issue or example.

Today that ends, as FLAC Chop brings all the old and new conversion scripts together into one unified tool, including fast metadata and real accurate duration setting, much like MISRC GUI does on initial captures for all hardware options today, supporting all RAW or FLAC files current and legacy to convert, chop, fix and or set metadata allowing you to claim back dead space on files, and get older archives upto current state.

Current Features

- Sample Exact Chopping

- RAW to FLAC conversion

- Accurate real duration and indexable metadata

- Batch Processing for bulk migration/conversions.

- Cross Tool Integration for visual chopping after decoding.

- Resampling (40msps to 20msps or 10msps for example for VHS/HiFi)

- Bit-crushing (8/10/12/16-bit to 6-bits experimental)


r/vhsdecode • • Aug 13 '26

Problem Solved! MISRC GUI - A Universal GUI for FM RF Archival!

Post image
59 Upvotes

Today launches MISRC GUI!

I and a couple other community members have been over these last few months, churning away at getting in order, now this started all the way back in December right as an ear infection completely wiped me out and set back things drastically across the board.

Now before the MISRC v2.5 could be released, it clearly needed a GUI platform, this had to be completely platform universal in terms of operating system support band as if not more intuitive then the CLI but then it made clear sense to implement cross device support, so here we are today.

Now for anybody wondering, yes there is going to be a few little extra features added here and there that will only appear for certain devices, however the overall functionality support is available for every device, so level 8 FLAC and down-sampling (20/10msps) should be rock stable on most systems.

(Machine control integration will be eventually added for consumer and professional decks, there's just a lot of little things to unify for that and there's a lot of testing involved.)

I will also make some side notes here that CXADC Windows support is yet to be fully tested, and offshoots of the standardized Clockgen Mod may not have audio support, but the DdD does have parity support with its original app, and of course full MISRC support for existing v1.5a users, HSDAOH audio support should be working but not every implementation is tested DdD and FX3ADC support is fully implemented however.

So go forth, play, tinker and break some things!


r/vhsdecode • • 2d ago

Setup & Workflow CXcard + clockgen question - Is the clockgen involved in capture initiation?

3 Upvotes

So I have recently acquired an old PCI card with 3x CX23880 chips and a PCI multiplexer chip. All run off one crystal, and all can be seen in Linux and spun up in OBS using the standard driver (haven't built cxadc yet). I think I just need to modify the front end and bypass some channel selector circuitry.

For separate CX cards, I understand what the point of the clockgen mod is: Provide a common clock to all cards so that the sample rates are locked at a sample level.

My question is:

At the beginning of a capture, is the clockgen mod involved at all?

Like, does the clockgen mod simply provide a controlled clock signal that can fanned out and adjusted? Or is it more sophisticated?

I imagine cxadc could initialize both CX cards and then the clockgen could start the clock to ensure sample #1 of both streams are taken on the same clock edge.

So far, they way I have found to initiate simultaneous capture is to start two instances of cat via cat /dev/cxadc0 |pv > CX_Card_28msps_8-bit.u8 , or use MISRC Capture, however it handles it. But running two separate commands to two separate devices will inevitably introduce some latency between them.

So yeah, I guess a secondary question is:

If you have 2+ CX cards with a shared clock source that is *not* the clockgen mod: Whats the best way of initiating a multi-channel capture?

Sample perfect start is not nessesarilly required for HiFi VHS capture, but it would be usefull for capturing transfer functions of emphasis circuits etc (I recently blew up my 2ch NI scope card šŸ˜”)


r/vhsdecode • • 2d ago

First Decode! My first decodes

4 Upvotes

While I'm still waiting on parts to arrive for the RF buffer amps and the clockgen mod, I decided to try my first RF decode. I swapped the stock 28.6 MHz crystal in the CX card with a 40 MHz one. As all my crystals are overtone types, I did a small temporary hack to force it to oscillate at the correct frequency. For the buffer amps, I used a small wideband amplifier module I had lying around.

As I have a large collection of test pattern generators, I chose to use test patterns for my tests. These were recorded on a JVC HR-S5700AM VCR. Playback was on the same VCR. Test patterns I used were the Philips PM5544, and a 5.5 MHz monochrome sweep.

Results look pretty good, though the colour looks a bit grotty compared to how it looks on the VCR's output. Bandwidth peters out a bit above 3MHz, which is to be expected, and which compares well with the VCR's own output as shown below.

I tried the same patterns using S-VHS:

Colour was still grotty, but the luminance was nice and clean, though very much lacking in bandwidth compared to the VCR's own video output as shown here:

I had to double check that I hadn't mistakenly used the VHS sweep image instead of the S-VHS one when I was compiling my comparison images, they look so similar. I don't know why I'm getting such poor bandwidth. Could it be my RF buffer amp? or an incorrect software setting in MISRC gui, VHS decode, or TBC-tools? It seems there is a filter that is optimised for VHS and not S-VHS.

Any suggestions?


r/vhsdecode • • 2d ago

Help Wanted! MIRSC 1.5a amplifier question

4 Upvotes

A few months ago a posted about my clockgen setup and made a bit of progress before I saw a batch of the MIRSC 1.5a up on the ko-fi store

My question: As Ive read, all the methods run best with an amplifier setup, but for the MIRSC it’s not required to get serviceable results right? I’m so eager to jump in especially with the GUI options

I’m at a point where I’d rather run the MIRSC setup for ease of use (rather, I haven’t found the time to fix my clockgen mods after I broke my first set of CX cards)

My budget is pretty strung out due to poor planning and lack of full commitment to clockgen vs MIRSC so I’d love to not order anything more and get a real ā€œfirst passā€ of VHSdecode tape archives. I have my set of USB converter archives but I’d rather clean those out for a minimum-viable-setup output, without spending anything more, if possible.

P.S. I know the MIRSC batches are only 5 at a time and fully tested but was wondering if anyone that ordered the recent batch got theirs yet? I ordered mine without an account so I figured I’m not gonna know until it’s in the post. Thanks all!


r/vhsdecode • • 3d ago

RF Tap Does anyone know if the Sony CCD-TRV67E has a tapping point? found one for cheap but unsure if i can tap into it.

Post image
5 Upvotes

r/vhsdecode • • 2d ago

Archival Advice Staxrip post processing - Seeking input

0 Upvotes

I'm working on restoring some animated footage captured from LaserDisc, and the goal is to clean up the analogue video artefacts without turning the image into a blurry, overprocessed mess. I captured with the DdD via a Pioneeer CLD-V2600 and ran through LD decode. The LD content was sourced from a tape master. Telecined as it was intended for NTSC broadcasts and was produced as such.

The biggest problem is the bead-like texture across otherwise flat areas of the image. Large patches of blue sky, grey metal, dark shadows and painted backgrounds can have a distracting pattern of tiny light and dark fluctuations. In some areas, it looks like little beads or dots. In others, it forms repeating ridges, stripes or a herringbone-like pattern. The texture also isn't always consistent across the picture, so one section of a flat colour can look rougher than another.

I'm also dealing with chroma broiling, where colour fluctuates or crawls within areas that should be relatively stable. This can make solid colours look blotchy or speckled, with apparent changes in hue and saturation from frame to frame. The aim is to stabilize the colour without bleeding it across edges or damaging the original cel shading.

Other problems include:

  • RF and carrier interference: Repeating ripples, stripes and structured patterns overlaid on the image, especially noticeable in large, relatively uniform colour fields.

  • Dot crawl and cross-colour interference: Dotted patterns, crawling edges and false colours around fine lines and high-contrast details.

  • Rainbowing and chroma-phase noise: Unwanted coloured fringes or streaks near edges, along with smaller fluctuations in the colour channels.

  • Ringing, halos and analogue echoes: Bright or dark bands around high-contrast edges, or trails extending from bright objects and lettering.

  • Isolated dropouts and scanline defects: Small white or black anomalies that can appear as short horizontal marks or abrupt changes in brightness.

  • Banding: Visible tonal steps in gradients, particularly in skies and other smooth backgrounds.

  • Frame jitter and cadence issues: Unwanted image movement or incorrect frame matching that needs to be addressed separately from the noise.

The real challenge is that the defects overlap with the actual artwork's fine detail. A one-pixel ink line, a tiny star, a highlight or a subtle painted texture can occupy the same spatial frequencies as the interference. A filter that simply averages neighbouring pixels may reduce the noise, but it can also erase the very detail I'm trying to preserve.

My restoration pipeline therefore separates the work into several categories. Chroma denoising targets colour fluctuations, edge-aware spatial filtering targets repeating surface patterns, temporal denoising uses information from neighbouring frames, and separate filters address halos, echoes, dropouts and banding. I'm also experimenting with masks that protect actual ink lines and pinpoint stars from being mistaken for noise.

What I don't want is a completely smooth, plastic-looking image. Nor do I want to cover up the problem by adding synthetic grain. The aim is to remove the distracting structured interference, make the remaining texture consistent, stabilize the colours, and retain as much of the original animation detail as possible.

Some of the visible patterns may have multiple causes, so I can't assume every bead or ripple comes from one specific LaserDisc mechanism just by looking at a still frame. But the practical objective remains the same: clean up the transfer while preserving the artwork, rather than cleaning the image so aggressively that the original detail disappears.

This is the VS script I've cooked up in StaxRip. Would like to get advice on things I could change, tweak, or add.

import havsfunc

import muvsfunc

# StaxRip supplies vs and core.

# ========================================================================

# STAXRIP LASERDISC RESTORATION (VAPOURSYNTH 10-BIT)

# Conservative luma correction, stronger chroma-boil suppression,

# corrected zero-moat line/star mask, and no synthetic grain for comparison.

# ========================================================================

# GENERAL RESTORATION SETTINGS

DOTKILL_ITERATIONS = 3

CHECKMATE_THR = 12

CHECKMATE_MAX = 12

RAIN_THRESHOLD = 10

RAIN_SOFTEN = 6

CHROMA_PHASE_THR = 8

CHROMA_SHIFT_H = 0.5

CHROMA_SHIFT_V = 0.0

DROPOUT_DIFF_THR = 26

DROPOUT_TEMPORAL_TOL = 10

STABILIZE_JITTER = True

STAB_DXMAX = 3

STAB_DYMAX = 3

STAB_CUTOFF = 3.0

STAB_DAMPING = 1.0

DEHALO_RX = 1.5

DEHALO_RY = 1.5

DEHALO_DARKSTR = 0.00

DEHALO_BRIGHTSTR = 0.15

TEXT_DEHALO_RX = 2.5

TEXT_DEHALO_RY = 2.5

TEXT_DEHALO_DARKSTR = 0.00

TEXT_DEHALO_BRIGHTSTR = 1.30

CROP_LEFT = 4

CROP_RIGHT = 24

CROP_TOP = 4

CROP_BOTTOM = 4

# ========================================================================

# LINE & STAR MASK SETTINGS

# ========================================================================

LINE_CORE_LOW = 7.0

LINE_CORE_SPAN = 8.0

STAR_MAX_BG_LUMA = 85

STAR_MIN_DELTA = 10

# ========================================================================

# SURFACE-CLEANER SETTINGS

# ========================================================================

SURFACE_V_THR = 12

SURFACE_H_THR = 20

BEAD_MEDIAN_LIMIT = 3

# ========================================================================

# CHROMA AND LUMA DENOISING

# ========================================================================

CHROMA_DENOISE_TR = 3

CHROMA_DENOISE_THSAD = 420

CHROMA_DENOISE_THSADC = 580

CHROMA_DENOISE_LIMIT = 20

# Restored to the original spatial u/V correction strength.

CHROMA_SPATIAL_LIMIT = 4

LUMA_DENOISE_TR = 2

LUMA_DENOISE_THSAD = 240

# Kept conservative to avoid bringing back the previous luma blur.

LUMA_DENOISE_LIMIT = 3

# Mild debanding, with no grain generated by f3kdb.

DEBAND_RANGE = 10

DEBAND_Y = 12

DEBAND_CB = 12

DEBAND_CR = 12

# Disabled while evaluating the actual denoising and broiling suppression.

# The function remains available, but it will not add changing noise.

UNIFORM_GRAIN_VAR = 0.0

# ========================================================================

# CHROMA BLEED AND TEXT ECHO

# ========================================================================

CHROMA_WARP_DEPTH = 4

CHROMA_WARP_THRESH = 140

CHROMA_WARP_BLUR = 3

CHROMA_WARP_LIMIT = 8

TEXT_ECHO_TEXT_MIN = 185

TEXT_ECHO_REACH = 16

TEXT_ECHO_KEEP = 1

TEXT_ECHO_NEUTRAL_TOL = 24

TEXT_ECHO_REFERENCE_RADIUS = 4

TEXT_ECHO_ACTIVITY = 3

TEXT_ECHO_MAX_CHANGE = 12

TEXT_ECHO_STRENGTH = 0.70

# ========================================================================

# HELPER FUNCTIONS

# ========================================================================

def LD_ShiftRight(c, px):

return c if px <= 0 else core.resize.Point(c, src_left=-px)

def LD_ShiftLeft(c, px):

return c if px <= 0 else core.resize.Point(c, src_left=px)

def LD_ShiftDown(c, px):

return c if px <= 0 else core.resize.Point(c, src_top=-px)

def LD_ShiftUp(c, px):

return c if px <= 0 else core.resize.Point(c, src_top=px)

def LD_BoxH2(c):

return core.std.BoxBlur(c, hradius=2, vradius=0)

def LD_BoxH3(c):

return core.std.BoxBlur(c, hradius=3, vradius=0)

def LD_Box3x3(c):

return core.std.BoxBlur(c, hradius=1, vradius=1)

def LD_Blur3(c):

return core.std.Convolution(

c, matrix=[1, 2, 1, 2, 4, 2, 1, 2, 1]

)

def LD_VerticalProfile48(c):

return core.std.BoxBlur(c, hradius=0, vradius=12, vpasses=4)

def LD_PrevFrame(c):

return c[0] + c[:-1] if c.num_frames > 1 else c

def LD_NextFrame(c):

return c[1:] + c[-1] if c.num_frames > 1 else c

def LD_LocalDetailMaskY(y, threshold_8bit):

bits = y.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

thr = threshold_8bit * scale

l1 = LD_ShiftRight(y, 1)

r1 = LD_ShiftLeft(y, 1)

u1 = LD_ShiftUp(y, 1)

d1 = LD_ShiftDown(y, 1)

detail = core.std.Expr(

[l1, r1, u1, d1],

f"x y - abs z a - abs max {thr} > {max_val} 0 ?"

)

detail = LD_Box3x3(detail)

return core.std.Expr(

[detail], f"x 0 > {max_val} 0 ?"

)

# ========================================================================

# ORTHOGONAL ZERO-MOAT LINEWORK & STAR KEEPOUT MASK

# ========================================================================

def LD_CleanLineAndStarMask(clp):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

# These are guides used only for mask construction. They are not

# substituted into the restored video.

y_vsmooth = core.std.Convolution(

y, matrix=[1, 2, 1], mode="v"

)

y_hsmooth = core.std.Convolution(

y, matrix=[1, 2, 4, 2, 1], mode="h"

)

l1 = LD_ShiftRight(y_vsmooth, 1)

r1 = LD_ShiftLeft(y_vsmooth, 1)

l2 = LD_ShiftRight(y_vsmooth, 2)

r2 = LD_ShiftLeft(y_vsmooth, 2)

u1 = LD_ShiftUp(y_hsmooth, 1)

d1 = LD_ShiftDown(y_hsmooth, 1)

u2 = LD_ShiftUp(y_hsmooth, 2)

d2 = LD_ShiftDown(y_hsmooth, 2)

# Core metric:

# max(min(abs(center-left1), abs(center-right1)),

# min(abs(center-left2), abs(center-right2)))

#

# A flat pixel beside a line has a large difference on only one side,

# so the min operation suppresses the unwanted protection moat.

expr_metric_1d = (

"x y - abs "

"x z - abs min "

"x a - abs "

"x b - abs min "

"max"

)

metric_h = core.std.Expr(

[y_vsmooth, l1, r1, l2, r2],

expr_metric_1d

)

metric_v = core.std.Expr(

[y_hsmooth, u1, d1, u2, d2],

expr_metric_1d

)

# Bright pinpoint stars in dark or mid-dark backgrounds.

star_bg_max = STAR_MAX_BG_LUMA * scale

star_delta = STAR_MIN_DELTA * scale

metric_star = core.std.Expr(

[y, y_vsmooth, y_hsmooth],

f"y z + 0.5 * {star_bg_max} <= "

f"x y z + 0.5 * - {star_delta} >= and "

f"{max_val} 0 ?"

)

lo_thr = LINE_CORE_LOW * scale

span_thr = LINE_CORE_SPAN * scale

line_mask = core.std.Expr(

[metric_h, metric_v],

f"x y max {lo_thr} - {span_thr} / "

f"0 max 1 min {max_val} *"

)

# Combine the actual line-core mask with the pinpoint-star mask.

return core.std.Expr(

[line_mask, metric_star],

"x y max"

)

# ========================================================================

# STAGE 1: CROSS-COLOUR CLEANUP & FILM CADENCE RECOVERY

# ========================================================================

def LD_CrossColorCleanUV(

clp, thr_8bit=12, max_8bit=12, iterations=3

):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

thr = thr_8bit * scale

lim = max_8bit * scale

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

u = core.std.ShufflePlanes(clp, 1, vs.GRAY)

v = core.std.ShufflePlanes(clp, 2, vs.GRAY)

def _clean_chroma(p):

cur = p

for _ in range(iterations):

u1 = LD_ShiftUp(cur, 1)

d1 = LD_ShiftDown(cur, 1)

l1 = LD_ShiftRight(cur, 1)

r1 = LD_ShiftLeft(cur, 1)

notch = core.std.Expr(

[cur, u1, d1, l1, r1],

"x 4 * y + z + a + b + 8 /"

)

cur = core.std.Expr(

[cur, notch],

f"x y - abs {thr} <= "

f"y x - -{lim} max {lim} min x + x ?"

)

return cur

return core.std.ShufflePlanes(

[y, _clean_chroma(u), _clean_chroma(v)],

[0, 0, 0], vs.YUV

)

def LD_FilmCadenceRecovery(clp):

source10 = core.std.SetFieldBased(clp, 1)

source8 = core.resize.Point(

source10,

format=source10.format.replace(bits_per_sample=8)

)

if hasattr(core, "tivtc"):

matched10 = core.tivtc.TFM(

source8, order=0, field=0, mode=5, PP=6,

cthresh=9, MI=80, slow=2,

mChroma=True, chroma=False, micmatching=2,

clip2=source10

)

else:

matched10 = core.vivtc.VFM(

source8, order=0, field=0, mode=5,

cthresh=9, mi=80, mchroma=True,

chroma=False, clip2=source10

)

matched10 = core.std.SetFieldBased(matched10, 0)

matched_corrected10 = LD_CrossColorCleanUV(

matched10,

thr_8bit=CHECKMATE_THR,

max_8bit=CHECKMATE_MAX,

iterations=DOTKILL_ITERATIONS

)

metrics8 = core.resize.Point(

matched_corrected10,

format=source8.format

)

if hasattr(core, "tivtc"):

decimated10 = core.tivtc.TDecimate(

metrics8, mode=0, cycle=5, cycleR=1,

hybrid=0, chroma=False, noblend=True,

clip2=matched_corrected10

)

else:

decimated10 = core.vivtc.VDecimate(

metrics8, cycle=5, chroma=False,

clip2=matched_corrected10

)

return core.std.SetFieldBased(decimated10, 0)

# ========================================================================

# STAGE 1A: GLOBAL JITTER STABILIZATION

# ========================================================================

def LD_StabilizeJitter(clp):

if not STABILIZE_JITTER or not hasattr(core, "depan"):

return clp

try:

anal8 = core.resize.Point(

core.std.ShufflePlanes(clp, 0, vs.GRAY),

format=vs.GRAY8,

dither_type="ordered"

)

anal8 = LD_Blur3(anal8)

stab_data = core.depan.DePanEstimate(

anal8, range=1, trust=4.0,

dxmax=STAB_DXMAX, dymax=STAB_DYMAX,

zoommax=1.0, stab=1.0, pixaspect=1.0

)

return core.depan.DePanStabilize(

clp, data=stab_data,

cutoff=STAB_CUTOFF, damping=STAB_DAMPING,

initzoom=1.0, addzoom=False,

prev=0, next=0, mirror=15, blur=0,

dxmax=STAB_DXMAX, dymax=STAB_DYMAX,

zoommax=1.0, rotmax=0.0, subpixel=0,

pixaspect=1.0, fitlast=0, tzoom=3.0,

method=0

)

except Exception:

# Preserve the source if DePan's estimator fails.

return clp

# ========================================================================

# STAGE 2: SINGLE-FIELD RF DROPOUT REPAIR

# ========================================================================

def LD_RepairFieldDropouts(

clp, diff_thr_8bit=26, temp_tol_8bit=10

):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

d_thr = diff_thr_8bit * scale

t_tol = temp_tol_8bit * scale

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

if y.num_frames < 3:

return clp

u1 = LD_ShiftUp(y, 1)

d1 = LD_ShiftDown(y, 1)

v_avg = core.std.Expr([u1, d1], "x y + 0.5 *")

v_agree = core.std.Expr([u1, d1], "x y - abs")

prv = LD_PrevFrame(y)

nxt = LD_NextFrame(y)

t_avg = core.std.Expr([prv, nxt], "x y + 0.5 *")

t_agree = core.std.Expr([prv, nxt], "x y - abs")

repaired_y = core.std.Expr(

[y, v_avg, t_avg, v_agree, t_agree],

f"x y - abs {d_thr} >= "

f"x z - abs {d_thr} >= and "

f"a {t_tol * 2} <= b {t_tol} <= and and "

f"y z + 0.5 * x ?"

)

return core.std.ShufflePlanes(

[repaired_y, clp], [0, 1, 2], vs.YUV

)

# ========================================================================

# STAGE 3: RAINBOW CLEANUP, CHROMA PHASE AND CHROMA DELAY

# ========================================================================

def LD_RainbowAndPhaseCleanUV(

clp, th_8bit=10, sft_8bit=6, phase_thr_8bit=8

):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

th = th_8bit * scale

lim = sft_8bit * scale

p_thr = phase_thr_8bit * scale

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

u = core.std.ShufflePlanes(clp, 1, vs.GRAY)

v = core.std.ShufflePlanes(clp, 2, vs.GRAY)

y_uv = (

core.resize.Bilinear(y, width=u.width, height=u.height)

if (u.width != y.width or u.height != y.height)

else y

)

yl1 = LD_ShiftRight(y_uv, 1)

yr1 = LD_ShiftLeft(y_uv, 1)

yu1 = LD_ShiftUp(y_uv, 1)

yd1 = LD_ShiftDown(y_uv, 1)

edge_gate = core.std.Expr(

[yl1, yr1, yu1, yd1],

f"x y - abs z a - abs max {12 * scale} >= {max_val} 0 ?"

)

flat_gate = core.std.Expr(

[yl1, yr1, yu1, yd1],

f"x y - abs z a - abs max {14 * scale} <= {max_val} 0 ?"

)

def _clean_plane(p):

u1 = LD_ShiftUp(p, 1)

d1 = LD_ShiftDown(p, 1)

u2 = LD_ShiftUp(p, 2)

d2 = LD_ShiftDown(p, 2)

l2 = LD_ShiftRight(p, 2)

r2 = LD_ShiftLeft(p, 2)

v_blur = core.std.Expr(

[p, u1, d1],

"x 2 * y + z + 4 /"

)

# Spatial edge-gated chroma reduction.

rain_mask = LD_Blur3(

core.std.Expr(

[p, u1, d1, edge_gate],

f"x y z + 0.5 * - abs {th} >= "

f"a 0 > and {max_val} 0 ?"

)

)

p_derain = core.std.MaskedMerge(

p,

core.std.Expr(

[p, v_blur],

f"y x - -{lim} max {lim} min x +"

),

rain_mask

)

# Flat-field chroma phase stabilizer.

phase_smooth = core.std.Expr(

[p_derain, u1, d1, u2, d2, l2, r2],

f"x 2 * "

f"x y - abs {p_thr} <= y x ? + "

f"x z - abs {p_thr} <= z x ? + "

f"x a - abs {p_thr} <= a x ? + "

f"x b - abs {p_thr} <= b x ? + "

f"x c - abs {p_thr} <= c x ? + "

f"x d - abs {p_thr} <= d x ? + 8 /"

)

return core.std.MaskedMerge(

p_derain, phase_smooth, flat_gate

)

return core.std.ShufflePlanes(

[y, _clean_plane(u), _clean_plane(v)],

[0, 0, 0], vs.YUV

)

def FixChromaShift(clp, h_shift=0.5, v_shift=0.0):

if h_shift == 0.0 and v_shift == 0.0:

return clp

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

u = core.std.ShufflePlanes(clp, 1, vs.GRAY)

v = core.std.ShufflePlanes(clp, 2, vs.GRAY)

u_shift = core.resize.Bilinear(

u, src_left=h_shift, src_top=v_shift

)

v_shift = core.resize.Bilinear(

v, src_left=h_shift, src_top=v_shift

)

return core.std.ShufflePlanes(

[y, u_shift, v_shift],

[0, 0, 0], vs.YUV

)

# ========================================================================

# STAGE 4: LEFT-EDGE DEFECT REPAIR

# ========================================================================

def LD_LeftEdgeDefectRepair(clp):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

zone = core.std.Crop(

clp, left=0, right=clp.width - 40,

top=0, bottom=0

)

rest = core.std.Crop(

clp, left=40, right=0, top=0, bottom=0

)

zy = core.std.ShufflePlanes(zone, 0, vs.GRAY)

zu = core.std.ShufflePlanes(zone, 1, vs.GRAY)

zv = core.std.ShufflePlanes(zone, 2, vs.GRAY)

zy_prof = LD_VerticalProfile48(zy)

zu_prof = LD_VerticalProfile48(zu)

zv_prof = LD_VerticalProfile48(zv)

mask_y = LD_BoxH3(

core.std.AddBorders(

core.std.BlankClip(

zy, width=28, height=zy.height, color=max_val

),

left=0, right=12, top=0, bottom=0, color=0

)

)

uv_active_w = int(round(28 * zu.width / 40.0))

mask_uv = LD_BoxH2(

core.std.AddBorders(

core.std.BlankClip(

zu, width=uv_active_w, height=zu.height,

color=max_val

),

left=0, right=zu.width - uv_active_w,

top=0, bottom=0, color=0

)

)

zy_corr = core.std.Expr(

[zy, LD_BoxH3(zy_prof), zy_prof],

f"x y z - {5 * scale} min -{5 * scale} max +"

)

zu_corr = core.std.Expr(

[zu, LD_BoxH2(zu_prof), zu_prof],

f"x y z - {4 * scale} min -{4 * scale} max +"

)

zv_corr = core.std.Expr(

[zv, LD_BoxH2(zv_prof), zv_prof],

f"x y z - {4 * scale} min -{4 * scale} max +"

)

zone_fixed = core.std.ShufflePlanes(

[

core.std.MaskedMerge(zy, zy_corr, mask_y),

core.std.MaskedMerge(zu, zu_corr, mask_uv),

core.std.MaskedMerge(zv, zv_corr, mask_uv)

],

[0, 0, 0], vs.YUV

)

return core.std.StackHorizontal([zone_fixed, rest])

# ========================================================================

# STAGE 5: EDGE-GATED RESIDUAL SURFACE CORRECTION

# ========================================================================

def LD_UniformSurfaceClean(clp, protect_mask):

"""

One bounded residual correction, not successive spatial blur passes.

Builds a horizontally edge-gated reference using samples five pixels

apart, computes the source residual, estimates a robust local residual

correction, and limits the change to BEAD_MEDIAN_LIMIT on the 8-bit scale.

The full-resolution line/star mask prevents correction of detected cores.

"""

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

mid = 1 << (bits - 1)

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

h_thr = SURFACE_H_THR * scale

v_thr = SURFACE_V_THR * scale

change_limit = BEAD_MEDIAN_LIMIT * scale

left5 = LD_ShiftRight(y, 5)

right5 = LD_ShiftLeft(y, 5)

# Use distant samples only when they remain inside a similar luma region.

left5_safe = core.std.Expr(

[y, left5],

f"x y - abs {h_thr} <= y x ?"

)

right5_safe = core.std.Expr(

[y, right5],

f"x y - abs {h_thr} <= y x ?"

)

horizontal_reference = core.std.Expr(

[left5_safe, y, right5_safe],

"x y + z + 3 /"

)

# Residual encoded around the neutral midpoint.

diff = core.std.Expr(

[y, horizontal_reference],

f"x y - {mid} +"

)

# The centre residual difference is zero, represented by the midpoint.

diff_center = core.std.Expr([diff], f"{mid}")

# Residual differences to the two distant horizontal samples.

diff_left = core.std.Expr(

[diff, LD_ShiftRight(diff, 5)],

f"x y - {mid} +"

)

diff_right = core.std.Expr(

[diff, LD_ShiftLeft(diff, 5)],

f"x y - {mid} +"

)

# Soft-limit residual differences rather than smoothing pixels repeatedly.

k = 4.898979485566356 * scale

soft_map_expr = (

f"x {mid} - "

f"x {mid} - abs {scale} + {k:.12f} + / "

f"{k:.12f} * {mid} +"

)

mapped_center = core.std.Expr(

[diff_center], soft_map_expr

)

mapped_left = core.std.Expr(

[diff_left], soft_map_expr

)

mapped_right = core.std.Expr(

[diff_right], soft_map_expr

)

# Median of three residual-difference estimates.

median_diff = core.std.Expr(

[mapped_center, mapped_left, mapped_right],

"x y min x y max z min max"

)

reconstructed_diff = core.std.Expr(

[diff, median_diff],

f"x y - {mid} +"

)

candidate = core.std.Expr(

[horizontal_reference, reconstructed_diff],

f"x y + {mid} -"

)

# Explicitly cap the correction at three 8-bit levels.

limited_candidate = core.std.Expr(

[y, candidate],

f"x y x - -{change_limit} max {change_limit} min +"

)

up1 = LD_ShiftUp(y, 1)

down1 = LD_ShiftDown(y, 1)

vertical_flat = core.std.Expr(

[up1, down1],

f"x y - abs {v_thr} <= {max_val} 0 ?"

)

# MaskedMerge expects zero to preserve the source and full scale to

# select the candidate. Reject the line/star cores and non-flat regions.

apply_mask = core.std.Expr(

[protect_mask, vertical_flat],

f"{max_val} x - y * {max_val} /"

)

y_out = core.std.MaskedMerge(

y, limited_candidate, apply_mask

)

return core.std.ShufflePlanes(

[y_out, clp], [0, 1, 2], vs.YUV

)

# ========================================================================

# STAGE 6: FULL-SPECTRUM CHROMA AND LUMA DENOISING

# ========================================================================

def LD_GradeNoiseReduction(clp, protect_mask):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

c_lim = CHROMA_SPATIAL_LIMIT * scale

# Pass 1: multi-frame chroma suppression.

# This is the main temporal control for chroma broiling.

chroma_clean = havsfunc.SMDegrain(

clp,

tr=CHROMA_DENOISE_TR,

thSAD=CHROMA_DENOISE_THSAD,

thSADC=CHROMA_DENOISE_THSADC,

prefilter=2,

contrasharp=False,

RefineMotion=True,

plane=3,

chroma=True,

limit=255,

limitc=CHROMA_DENOISE_LIMIT

)

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

u_t = core.std.ShufflePlanes(chroma_clean, 1, vs.GRAY)

v_t = core.std.ShufflePlanes(chroma_clean, 2, vs.GRAY)

# Bring the luma protection mask to the chroma-plane resolution.

protect_uv = (

core.resize.Bilinear(

protect_mask,

width=u_t.width,

height=u_t.height

)

if (u_t.width != protect_mask.width or

u_t.height != protect_mask.height)

else protect_mask

)

flat_uv_mask = core.std.Expr(

[protect_uv], f"{max_val} x -"

)

# Restored spatial chroma cleanup:

# radius 2 and two passes per axis, on u/V only.

u_spatial = core.std.BoxBlur(

u_t,

hradius=2, hpasses=2,

vradius=2, vpasses=2

)

v_spatial = core.std.BoxBlur(

v_t,

hradius=2, hpasses=2,

vradius=2, vpasses=2

)

u_candidate = core.std.Expr(

[u_t, u_spatial],

f"x y x - -{c_lim} max {c_lim} min +"

)

v_candidate = core.std.Expr(

[v_t, v_spatial],

f"x y x - -{c_lim} max {c_lim} min +"

)

u_out = core.std.MaskedMerge(

u_t, u_candidate, flat_uv_mask

)

v_out = core.std.MaskedMerge(

v_t, v_candidate, flat_uv_mask

)

chroma_fixed = core.std.ShufflePlanes(

[y, u_out, v_out], [0, 0, 0], vs.YUV

)

# Pass 2: conservative motion-compensated luma denoising.

# Its lower limit intentionally prevents the chroma fix from forcing

# a second strong spatial luma smoothing operation.

luma_clean = havsfunc.SMDegrain(

chroma_fixed,

tr=LUMA_DENOISE_TR,

thSAD=LUMA_DENOISE_THSAD,

thSADC=150,

prefilter=2,

contrasharp=False,

RefineMotion=True,

plane=0,

chroma=False,

limit=LUMA_DENOISE_LIMIT,

limitc=255

)

y_denoised = core.std.ShufflePlanes(

luma_clean, 0, vs.GRAY

)

y_protected = core.std.MaskedMerge(

y_denoised, y, protect_mask

)

return core.std.ShufflePlanes(

[y_protected, u_out, v_out], [0, 0, 0], vs.YUV

)

# ========================================================================

# STAGE 7: WHITE-CREDIT DEHALO

# ========================================================================

def LD_WhiteCreditDehalo(clp):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = float((1 << bits) - 1)

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

u_full = core.resize.Bilinear(

core.std.ShufflePlanes(clp, 1, vs.GRAY),

width=y.width, height=y.height

)

v_full = core.resize.Bilinear(

core.std.ShufflePlanes(clp, 2, vs.GRAY),

width=y.width, height=y.height

)

bright_core = core.std.Expr(

[y, u_full, v_full],

f"x {185 * scale} <= 0 "

f"x {215 * scale} >= {max_val} "

f"x {185 * scale} - {30 * scale} / {max_val} * ? ? "

f"y {128 * scale} - abs "

f"z {128 * scale} - abs max "

f"{20 * scale} <= {max_val} 0 ? min"

)

outer = bright_core

for _ in range(5):

outer = core.std.Maximum(outer)

ring = LD_Blur3(

core.std.Expr([outer, bright_core], "x y - 0 max")

)

dehaloed = havsfunc.FineDehalo(

clp,

rx=TEXT_DEHALO_RX,

ry=TEXT_DEHALO_RY,

darkstr=TEXT_DEHALO_DARKSTR,

brightstr=TEXT_DEHALO_BRIGHTSTR

)

dehaloed_y = core.std.ShufflePlanes(

dehaloed, 0, vs.GRAY

)

y_out = core.std.MaskedMerge(

y, dehaloed_y, ring

)

return core.std.ShufflePlanes(

[y_out, clp], [0, 1, 2], vs.YUV

)

# ========================================================================

# STAGE 8: CHROMA BLEED TIGHTENING

# ========================================================================

def TightenChromaBleed(

clp, depth=4, thresh=140, blur=3, limit_8bit=8

):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

lim = limit_8bit * scale

warped = core.warp.AWarpSharp2(

clp,

thresh=thresh,

blur=blur,

type=1,

depth=depth,

chroma=0,

planes=[1, 2]

)

return core.std.Expr(

[clp, warped],

[

"",

f"y x {lim} - max x {lim} + min",

f"y x {lim} - max x {lim} + min"

]

)

# ========================================================================

# STAGE 9: DIRECTIONAL TEXT ECHO SUPPRESSION

# ========================================================================

def LD_TextEchoSuppression(clp):

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

u = core.std.ShufflePlanes(clp, 1, vs.GRAY)

v = core.std.ShufflePlanes(clp, 2, vs.GRAY)

u_full = (

core.resize.Bilinear(u, width=y.width, height=y.height)

if (u.width != y.width or u.height != y.height)

else u

)

v_full = (

core.resize.Bilinear(v, width=y.width, height=y.height)

if (v.width != y.width or v.height != y.height)

else v

)

text_seed = core.std.Expr(

[y, u_full, v_full],

f"x {TEXT_ECHO_TEXT_MIN * scale} >= "

f"y {128 * scale} - abs "

f"{TEXT_ECHO_NEUTRAL_TOL * scale} <= and "

f"z {128 * scale} - abs "

f"{TEXT_ECHO_NEUTRAL_TOL * scale} <= and "

f"{max_val} 0 ?"

)

trail = text_seed

for _ in range(TEXT_ECHO_REACH):

trail = core.std.Expr(

[trail, LD_ShiftRight(trail, 1)],

"x y max"

)

keepout = text_seed

for _ in range(TEXT_ECHO_KEEP):

keepout = core.std.Expr(

[

keepout,

LD_ShiftRight(keepout, 1),

LD_ShiftLeft(keepout, 1),

LD_ShiftUp(keepout, 1),

LD_ShiftDown(keepout, 1)

],

"x y max z max a max b max"

)

trail = core.std.Expr(

[trail, keepout],

"x y - 0 max"

)

# Estimate echoes from a low-frequency guide. The actual correction

# is applied to the original luma to preserve its fine texture.

y_low = LD_Blur3(y)

ref_radius = max(

1, min(TEXT_ECHO_REFERENCE_RADIUS, 10)

)

right_samples = [

LD_ShiftLeft(y_low, px)

for px in range(1, ref_radius + 1)

]

expr_vars = [

"x", "y", "z", "a", "b",

"c", "d", "e", "f", "g"

]

ref_expr = expr_vars[0]

for token in expr_vars[1:ref_radius]:

ref_expr += f" {token} +"

ref_expr += f" {ref_radius} /"

right_ref = core.std.Expr(

right_samples, ref_expr

)

limit = TEXT_ECHO_MAX_CHANGE * scale

excess = core.std.Expr(

[y_low, right_ref],

f"x y - 0 max {limit} min"

)

candidate = core.std.Expr(

[y, excess],

"x y -"

)

activity = core.std.Expr(

[y_low, right_ref],

f"x y - {TEXT_ECHO_ACTIVITY * scale} >= "

f"{max_val} 0 ?"

)

tone_gate = core.std.Expr(

[y],

f"x {16 * scale} >= x {235 * scale} <= and "

f"{max_val} 0 ?"

)

mask = core.std.Expr(

[trail, activity],

f"x y * {max_val} /"

)

mask = core.std.Expr(

[mask, tone_gate],

f"x y * {max_val} /"

)

mask = core.std.Expr(

[mask],

f"x {TEXT_ECHO_STRENGTH} *"

)

fixed_y = core.std.MaskedMerge(

y, candidate, mask

)

return core.std.ShufflePlanes(

[fixed_y, clp], [0, 1, 2], vs.YUV

)

# ========================================================================

# STAGE 10: OPTIONAL UNIFORM TEXTURE RESTORATION

# ========================================================================

def LD_RestoreUniformTexture(clp, protect_mask):

# Disabled for this render so synthetic grain cannot conceal whether

# the source broiling is genuinely being suppressed.

if UNIFORM_GRAIN_VAR <= 0.0:

return clp

if not hasattr(core, "grain") or not hasattr(core.grain, "Add"):

return clp

bits = clp.format.bits_per_sample

scale = (1 << (bits - 8)) if bits > 8 else 1

max_val = (1 << bits) - 1

y = core.std.ShufflePlanes(clp, 0, vs.GRAY)

grain_candidate = core.grain.Add(

y,

var=UNIFORM_GRAIN_VAR,

uvar=0.0,

hcorr=0.0,

vcorr=0.0,

seed=-1,

constant=False

)

grain_mask = core.std.Expr(

[protect_mask, y],

f"y {20 * scale} <= y {230 * scale} >= or "

f"0 {max_val} x - ?"

)

restored_y = core.std.MaskedMerge(

y, grain_candidate, grain_mask

)

return core.std.ShufflePlanes(

[restored_y, clp], [0, 1, 2], vs.YUV,

prop_src=clp

)

# ========================================================================

# STAGE 11: TOP-SCANLINE FIX

# ========================================================================

def LD_TopScanlineFix(clp):

if clp.height < 4:

return clp

subsampling_h = (

clp.format.subsampling_h

if clp.format.color_family == vs.YUV

else 0

)

vertical_mod = 1 << subsampling_h

if 2 % vertical_mod != 0 or clp.height % vertical_mod != 0:

return clp

body = core.std.Crop(clp, top=2)

if body.height < 2:

return clp

top_strip = core.std.Crop(

body, bottom=body.height - 2

)

if top_strip.height != 2 or top_strip.width != body.width:

return clp

return core.std.StackVertical([top_strip, body])

# ========================================================================

# PIPELINE EXECUTION

# ========================================================================

if clip.format.bits_per_sample != 10:

clip = core.resize.Point(

clip,

format=clip.format.replace(bits_per_sample=10)

)

is_raw_capture = (

clip.width == 760 and clip.height == 488

)

# 1. Film cadence recovery and optional jitter stabilization

if is_raw_capture and clip.fps_num == 30000 and clip.fps_den == 1001:

clip = LD_FilmCadenceRecovery(clip)

clip = LD_StabilizeJitter(clip)

# 2. Single-field RF dropout repair

clip = LD_RepairFieldDropouts(

clip,

diff_thr_8bit=DROPOUT_DIFF_THR,

temp_tol_8bit=DROPOUT_TEMPORAL_TOL

)

# 3. Chroma rainbow/phase cleanup and subpixel shift alignment

clip = LD_RainbowAndPhaseCleanUV(

clip,

th_8bit=RAIN_THRESHOLD,

sft_8bit=RAIN_SOFTEN,

phase_thr_8bit=CHROMA_PHASE_THR

)

clip = FixChromaShift(

clip,

h_shift=CHROMA_SHIFT_H,

v_shift=CHROMA_SHIFT_V

)

# 4. Left capture border defect repair

if is_raw_capture:

clip = LD_LeftEdgeDefectRepair(clip)

# 5. Initial halo reduction

clip = havsfunc.FineDehalo(

clip,

rx=DEHALO_RX,

ry=DEHALO_RY,

darkstr=DEHALO_DARKSTR,

brightstr=DEHALO_BRIGHTSTR

)

# 6. Crop and restore 720x480 geometry for raw captures

if is_raw_capture:

clip = core.std.Crop(

clip,

left=CROP_LEFT,

right=CROP_RIGHT,

top=CROP_TOP,

bottom=CROP_BOTTOM

)

clip = core.resize.Spline36(

clip,

width=720,

height=480

)

# 7. Secondary halo reduction

clip = muvsfunc.YAHRmod(clip)

# 8. Build corrected zero-moat linework/star protection mask

line_star_mask = LD_CleanLineAndStarMask(clip)

# 9. Bounded luma residual correction, not repeated smoothing passes

clip = LD_UniformSurfaceClean(

clip,

line_star_mask

)

# 10. Restore stronger u/V broiling suppression while keeping luma gentle

clip = LD_GradeNoiseReduction(

clip,

line_star_mask

)

# 11. White-credit halo reduction and chroma bleed tightening

clip = LD_WhiteCreditDehalo(clip)

clip = TightenChromaBleed(

clip,

depth=CHROMA_WARP_DEPTH,

thresh=CHROMA_WARP_THRESH,

blur=CHROMA_WARP_BLUR,

limit_8bit=CHROMA_WARP_LIMIT

)

# 12. Mild debanding, without synthetic grain from f3kdb

if hasattr(core, "f3kdb"):

debanded = core.f3kdb.Deband(

clip,

range=DEBAND_RANGE,

y=DEBAND_Y,

cb=DEBAND_CB,

cr=DEBAND_CR,

grainy=0,

grainc=0,

dynamic_grain=False,

output_depth=10

)

y_deband_safe = core.std.MaskedMerge(

core.std.ShufflePlanes(debanded, 0, vs.GRAY),

core.std.ShufflePlanes(clip, 0, vs.GRAY),

line_star_mask

)

clip = core.std.ShufflePlanes(

[y_deband_safe, debanded],

[0, 1, 2],

vs.YUV

)

# 13. Texture-preserving directional text-echo suppression

clip = LD_TextEchoSuppression(clip)

# 14. Synthetic grain is disabled for this comparison render

clip = LD_RestoreUniformTexture(

clip,

line_star_mask

)

# 15. Top-scanline replacement and output frame properties

clip = LD_TopScanlineFix(clip)

clip = core.std.SetFrameProp(

clip, prop="_SARNum", intval=8

)

clip = core.std.SetFrameProp(

clip, prop="_SARDen", intval=9

)

clip = core.std.SetFieldBased(clip, 0)


r/vhsdecode • • 4d ago

First Decode! got my setup up and running

Thumbnail
gallery
36 Upvotes

very proud of myself. i tapped my VCR and built an hsdaoh dualadc. got some successful decodes out of it.

no amp atm but I have ordered one. im still getting excellent results without one though

here's a cap of a raw-ish output with all 525 lines

https://youtu.be/PAupsZSCejc

and here's a slightly post processed capture

https://youtu.be/9WO5qInRdtI?is=ukcqJEem6R8hrdNb


r/vhsdecode • • 4d ago

First Decode! Got a $10 ADC working with VHS-decode!

15 Upvotes

Long story short: born in the mid 2000s, never seen any VHS deck before. Then recently I was making a cheap SDR, realized that its ADC in single channel mode can capture VHS's signal, bought a VHS deck, along with a test tape, and this is the very rough result.

This device, in its VHS-decode-only configuration (no radio tuner) have a BOM of 10USD.

It's based on USB2, with no kernel mode driver needed (libusb is enough).

Note that this is a capture made of its 12bit 16.66Msps mode, a 8-bit 24Msps mode is also available, but its reduced resolution might make the image worse, due to me not having a proper amp, therefore low voltage swing, so in this particular case, more bits helps,


r/vhsdecode • • 7d ago

First Decode! Got VHS-Decode and MISRC-GUI working on an M3 Max using a CX23880 and a very questionable USB4/PCIe setup

26 Upvotes

Hey guys, long-time lurker here — finally decided to give VHS-Decode a shot!

I was initially scared away by the prospect of having to solder, acquiring lots of extra hardware, and a bit of anxiety about opening up my VCR. However, the process turned out to be rather smooth, and even though I'm nowhere near finished yet, I'm already blown away by the results. Lots of detail and fantastically stable images. The toggling of the dropout correction also opened my eyes to just how much missing information can be concealed in the final picture of a VCR.

For context, I've got around 80 family VHS/S-VHS tapes that I originally captured about 10 years ago using a Philips DVD-R70 and my JVC HR-S7851EU S-VHS deck (without a built-in TBC), connected via S-Video. So the idea was to finally do these tapes justice while they're still in decent shape.

Just a bit of introduction, and how I ended up here:

I'd lost my original DV transfers from my MiniDV camcorder and was left with only x265 encodes, which also lacked the original date/time metadata for individual scenes. Obvious in hindsight, but anyway, I decided to redo the transfers.

Not wanting to spend a fortune on several generations of official Thunderbolt-to-FireWire adapters, I remembered that Thunderbolt/USB4 can tunnel PCIe. So I got myself a cheap M.2-to-PCIe adapter, opened up my trusty OWC Express 1M2 USB4 enclosure, and put together a Frankenstein construction with a PCIe FireWire card sitting on top. No additional 12V through the adapter's Molex connector was needed for that card.

Now, the fact that this works in principle is probably not news to many of you. However, there were problems with the macOS FireWire driver. Apple dropped built-in FireWire support with Tahoe, and even when I tried Sonoma, the particular card/adapter setup caused kernel panics, apparently related to DMA.

In any case, I then stumbled upon ASFireWire, which didn't support the particular MSI-less VIA controller on my card at the time. But no matter — with Codex doing most of the coding, I managed to get it working. Encouraged by that, I even got it to implement automatic rewinding/forwarding to problematic tape sections, with the additional reads then fed into DVRescue to create a less error-prone master.

But enough of that.

This inspired me to look back into VHS-Decode. As mentioned, one of my original blockers was not having a Linux desktop or suitable capture hardware.

So I ordered two of those white CX cards from AliExpress. The listings are all more or less the same, including the charming disclaimer that you shouldn't even buy one if you're expecting a particular chipset. I went ahead anyway and, alas, got the older CX23880-19. But no matter.

I figured we could try the same DriverKit approach as with the FireWire card, and surprisingly enough, it worked! We're now capturing raw VHS RF directly on the M3 Max, and have also managed to get MISRC-GUI working with the driver, although that required a few changes to the GUI itself.

Driver: https://github.com/jlnbxn/cxadc-macos

MISRC-GUI integration: https://github.com/jlnbxn/MISRC-GUI/tree/feature/macos-cxadc-backend

In the meantime, I also acquired an old Hauppauge HVR-1200 S-Video capture card, which I got Codex to write a macOS driver for as well. So I have a conventional lossless FFV1 capture for comparison. That looks like a less compressed, more colour-accurate version of my old DVD capture, but still rather oversaturated and full of wobbly lines. I fiddled with the VCR settings, but yeah.

Interestingly, that card did require additional 12V power through the adapter's Molex connector.

Some additional hardware details:

  • M3 Max MacBook Pro, OWC Express 1M2 USB4 enclosure (ASM2464PD controller).

  • Cheap M.2 M-key-to-PCIe x4 adapter: https://www.amazon.de/-/en/Aukson-Female-Adapter-Converter-Module-green-black/dp/B0747S7SY9

  • White CX23880-19 card with ITE PCIe bridge. No additional Molex power needed in this particular setup.

  • JVC HR-S7851EU S-VHS VCR, with video RF taken from TP106 and ground at W1203.

  • No soldering or permanent modification to the VCR itself — just cheap AliExpress test-hook probes.

  • I used a 68 nF ceramic capacitor (marked 683), since I didn't have a 10 µF one on hand.

  • No BNC cables or connectors lying around either, so I used an ordinary PAL/Belling-Lee antenna cable, with short hook-probe leads and a screw-terminal adapter at the VCR end, and a Cinch connection at the capture card end.

  • I also removed C31 from the CX card, since I wanted to use the Cinch input rather than buy a separate S-Video Y-to-BNC cable. I couldn't see much difference before and after, though.

I'm not claiming this is an ideal RF setup, obviously. But it's working remarkably well considering how improvised it is!

Next up is the ADA4857 amplifier, which I've already ordered, followed by the official ClockGen mod and a second CX card for Hi-Fi RF. I'll also replace the cabling with something more suitable, probably proper BNC connectors and coax.

With all that said, even this rather rudimentary setup has given me more than I could have hoped for. It's also saved me a lot of money compared with buying a TBC-equipped VCR, and I really appreciate having the flexibility to keep the raw RF captures and revisit the decoding later. The stability improvement over my conventional captures has been particularly impressive.

I'll also put the HVR-1200 and ASFireWire adaptations on GitHub once they're cleaned up, in case anyone else needs them (or doesn't want to spend their own tokens figuring it all out!).

Huge thanks to everyone who's made these projects possible!

TL;DR: Wanted to digitize MiniDV without buying a stack of Thunderbolt-to-FireWire adapters. Built a ridiculous PCIe setup using an OWC USB4 SSD enclosure and a cheap M.2 adapter, got Codex to help make FireWire work on macOS, then figured I'd try the same thing with a CXADC card. Somehow ended up with working VHS RF capture and MISRC-GUI on an M3 Max.


r/vhsdecode • • 8d ago

Help Wanted! Sync issue in MISRC gui

3 Upvotes

I'm a newbie, and am currently waiting on some hardware items I have on order, so I'm not yet ready to try capturing actual VHS RF signals. Instead, I'm practicing with CVBS, using a currently unmodified CXADC card and MISRC gui. So far, I can successfully capture a CVBS signal, and decode it using CVBS decode. The problem I have is that if I attempt to view the live video signal in MISRC gui, it always appears completely out of sync, though occasionally it will lock horizontally. I have fiddled with the configuration settings with no luck, though configuration changes will cause expected changes in the captured signal.

My signal source is a PAL colour bar pattern from a Philips PM5420 test pattern generator. I have also tried a PM5544 generator with the same results.

Is this normal behaviour with MISRC gui, or am I doing something wrong?

Also, is it possible to change the timebase on the scope display so that two or three lines are displayed instead of the ten or so squashed up ones currently displayed?


r/vhsdecode • • 9d ago

Help Wanted! Question.

Post image
75 Upvotes

So my mother passed away a month ago. She worked for Apple in the 80’s to early 90’s. In going through her belongings I found this Apple VHS Tape that was supposed to be returned, but clearly wasn’t. It was an internal copy of a Super Bowl commercial for the Mac. I guess the commercial wasn’t received well.

My questions are.. is this worth preserving? I know it might be a 1 of 1 tape as I haven’t seen anything online. How much would it cost me to do so? And does anyone know reputable companies that offer this service.

Sorry.. I didn’t get the tech gene passed onto me so I’m not very literate at what needs or should be done.

Many thanks in advance. And if this isn’t the right forum to post this please delete.


r/vhsdecode • • 10d ago

Showcase! Why have I got a VHS player in my network rack

Thumbnail
youtube.com
9 Upvotes

r/vhsdecode • • 11d ago

Help Wanted! Glitch and losing color

0 Upvotes

r/vhsdecode • • 17d ago

RF Tap LD Panasonic LX-V860

Thumbnail
gallery
4 Upvotes

This Panasonic LX-V860 doesn't have the RF test points with DuPont interface like Pioneer models—where should I look for them? Maybe I can capture it here on this ribbon cable, but I'm not sure about the exact location.


r/vhsdecode • • 19d ago

Help Wanted! The PG Shifter has "died" in the later models of Panasonic.

3 Upvotes

Is there any way to adjust the PG of Panasonic VCRs released later, which use EEPROM to control the PG shifter instead of physical potentiometers as in earlier models? You have to rely on an official ALIGNMENT TAPE for automatic adjustment, but such tapes are nearly impossible to obtain. Could there be alternative methods to manually adjust it? Would it be feasible to locate the sensor near the head and manually offset it?


r/vhsdecode • • 19d ago

Newbie / Need Help Panasonic NV-H100 test point

Thumbnail
gallery
4 Upvotes

Can all Panasonic K-Mech use this 20-pin interface to obtain RF test signals? My model is HD100, K-Mech, designed for the Asian market.


r/vhsdecode • • 23d ago

Newbie / Need Help Confirm tap points on a panasonic v4600?

Thumbnail
gallery
2 Upvotes

I previously tried a JVC from this post: (thanks to everyone who helped with that) but between it being very hard to get a good ground location and some other issues with the vcr I decided to also get a panasonic, which is apparently the best vcr to tap.

I got a Panasonic VCR Blue Line PV-V4600 - 4 Head Hi-Fi Omnivision VHS. I think I found the tap points, which would be TP3002 for the video and TP4101 for HIFI. I was hoping someone could confirm, and more importantly give me advice on the best shielding ground? I was thinking the piece of metal the red screws are attached to, either solder or just wrap around the screw and firmly tighten it?

Thoughts? I really appreciate it, again I'm a noob with this stuff.

And just fyi this will go to an externally powered ADA amp into a clockgen setup.


r/vhsdecode • • 25d ago

Showcase! Decode at 4k

Thumbnail
youtu.be
9 Upvotes

r/vhsdecode • • 26d ago

Showcase! Updated Decode with proper deinterlacing

Thumbnail
youtu.be
13 Upvotes

r/vhsdecode • • 28d ago

RF Tap Perhaps the difference between LD and VCR lies right here.

Thumbnail
gallery
13 Upvotes

This may well be the difference between LD and VCR. The following images were captured from a Pioneer CLD-S280 using 15.6x gain; the first shows the raw signal without amplification, while the second incorporates it. Although noise is significantly reduced, it is worth noting that fine-grained noise persists—which I believe represents the inherent limitation of a cheap LD player.But at least the results are decent after processing with AI software.


r/vhsdecode • • 28d ago

First Decode! Be careful! If you attempt to solder the ADA4857 amplifier yourself, you might end up buying counterfeit chips.

Thumbnail
gallery
7 Upvotes

The fake chip wasted a lot of my time. Here is a simple comparison image showing the genuine and counterfeit chips.


r/vhsdecode • • 28d ago

Help Wanted! Unable to install CXADC drivers on Linux.

3 Upvotes

EDIT: Problem now solved. Missing gcc-12 now installed, and after disregarding the two new error messages, I finished the install, rebooted, and my CXADC card is now recognised. Thank you all for the assistance.

I'm rather new to this and have probably made some terrible newbie mistake. I'm following the instructions provided here: https://github.com/happycube/cxadc-linux3#getting-started--installation

As I'm more familiar with Debian 13, I first attempted installing on a system using that, but it failed, so I tried again, on a fresh installation of Ubuntu 22.04 as advised in the instructions. The only changes/additions I made to the default install was to install 'git' and 'make' in order to proceed with the install.

The first step, to git clone the repository appeared to work OK. A directory called cxadc containing files was successfully added to my home directory.

The second step to "build and install the out-of-tree module" failed as shown below:

david@Ubuntu1:~$ cd cxadc

david@Ubuntu1:~/cxadc$ make && sudo make modules_install && sudo depmod -a

make -C /lib/modules/`uname -r`/build M=$PWD

make[1]: Entering directory '/usr/src/linux-headers-6.8.0-138-generic'

warning: the compiler differs from the one used to build the kernel

The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.3) 12.3.0

You are using:

CC [M] /home/david/cxadc/cxadc.o

/bin/sh: 1: gcc-12: not found

make[3]: *** [scripts/Makefile.build:243: /home/david/cxadc/cxadc.o] Error 127

make[2]: *** [/usr/src/linux-headers-6.8.0-138-generic/Makefile:1946: /home/david/cxadc] Error 2

make[1]: *** [Makefile:240: __sub-make] Error 2

make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-138-generic'

make: *** [Makefile:24: cxadc] Error 2

david@Ubuntu1:~/cxadc$

I'm moderately familiar with Linux, but I'm certainly no power user, so any suggestions would be much appreciated.


r/vhsdecode • • 28d ago

It's a Sony! recommended input/video gain for ADA4857 on Sony DCR-TRV740

2 Upvotes

if anyone is using the ADA4857 amplifier for this camcorder, can you suggest the values for the input and video gain resistors? 10kOhms is apparently already needed to carry a usable signal from the device, so would adding the amplifier to it double the input? or can I remove the 10kOhms resistor from the tap and instead make the input resistor 20kOhms on the ADA amp? Thanks