●MODEL — Gemini 3.8 Flash reached general availability on September 2 under the API model ID gemini-3.8-flash. With 3.7 Flash having gone GA on August 13, that is a three-week turnaround●PRICE — Pricing holds at $0.75 input and $3.75 output per MTok, unchanged from 3.7 Flash. Speed and cost stay put; reasoning and coding are what moved●BENCH — The gap shows up in coding. Terminal-Bench 2.1 goes from 81.6% on 3.7 Flash to 90.8%, and Google reports gains on finance and legal agent benchmarks as well●REACH — Distribution is unusually broad. Beyond AI Studio and the Gemini API, it is now the default model in Antigravity, and it ships in Android Studio, Stitch, AI Mode, and Sheets●ASSISTANT — Google begins removing Assistant from Android today, September 4. The rollout takes a few weeks and cannot be undone once it reaches a device; Interpreter mode is among the pieces Gemini still lacks●CODE — Gemini Advanced can now take a whole code repository uploaded from your device, capped at one folder per conversation with up to 1,000 files or 100MB●MODEL — Gemini 3.8 Flash reached general availability on September 2 under the API model ID gemini-3.8-flash. With 3.7 Flash having gone GA on August 13, that is a three-week turnaround●PRICE — Pricing holds at $0.75 input and $3.75 output per MTok, unchanged from 3.7 Flash. Speed and cost stay put; reasoning and coding are what moved●BENCH — The gap shows up in coding. Terminal-Bench 2.1 goes from 81.6% on 3.7 Flash to 90.8%, and Google reports gains on finance and legal agent benchmarks as well●REACH — Distribution is unusually broad. Beyond AI Studio and the Gemini API, it is now the default model in Antigravity, and it ships in Android Studio, Stitch, AI Mode, and Sheets●ASSISTANT — Google begins removing Assistant from Android today, September 4. The rollout takes a few weeks and cannot be undone once it reaches a device; Interpreter mode is among the pieces Gemini still lacks●CODE — Gemini Advanced can now take a whole code repository uploaded from your device, capped at one folder per conversation with up to 1,000 files or 100MB
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.
I was listening to a freshly swapped ambient track on earphones before bed. It loops roughly every ten seconds, and at that one wrap point there is a very small click.
On my desk speakers I hear nothing. I checked again the next morning on speakers, decided I had imagined it, and then noticed it again that night on earphones. I went around that loop twice before I stopped and looked at it properly.
The healing sound app I run as an indie developer is used with a loop playing for hours while someone sleeps. A step too small to catch once will still reach somebody's ear after four hundred repetitions.
What audio understanding gave back, and what it did not
I had a Gemini API setup on hand, so I sent the whole track first, with a plain request: point out anything that sounds unnatural, with timestamps.
What came back was a calm description of texture and mood. The weight of the low end, the length of the tail, the monotony of the repetition. All of it reasonable, and not one word about the sub-ten-millisecond step I was actually looking for.
Rewording the prompt twice more changed nothing. Even when I wrote "click" and "discontinuity" explicitly, the model kept assembling its answer from the musical side.
In hindsight that is exactly right. Audio understanding lives in the layer of meaning and impression. It is not the instrument for asking whether two adjacent sample values jump. I had misread which tool owned which layer, and spent two evenings blaming my prompt.
I count whether the waveform is broken; I ask Gemini whether it sounds broken. Having those two in the wrong order was the whole cost.
An absolute threshold does not survive contact with real material
The step itself is trivial to compute — the difference between the last sample and the first.
Putting a threshold on that absolute value, though, did not work for me. A quiet pad and a rain-like bed behave completely differently in terms of how much the signal moves from sample to sample under normal conditions.
In rain, neighbouring samples are always moving a lot. A step of 0.05 sitting in that is buried. Put the same 0.05 into a quiet pad and you get an audible click.
So I divide the step by how much that particular track normally moves. For the denominator I take the 99.9th percentile of the absolute first difference, rather than the maximum, so a single incidental thump baked into the source does not drag the reference up.
Metric
Meaning
How I use it
jump
Absolute difference between last and first sample
Context only. Never a verdict on its own
p999_step
99.9th percentile of adjacent sample differences
The track's own ceiling for normal motion
ratio
jump ÷ p999_step
The verdict metric. Above 1, a human listens
edge_rms
RMS of the first and last 10 ms
Separates tracks whose edges are silent
I keep edge_rms for a reason. A track that starts and ends in silence has a step of essentially zero, so ratio always passes. That is not safety — it simply never entered the test. Conflating the two lets every fade-in track slide straight through. When SILENT_EDGE comes back I switch tests entirely and check the loop length and the silence duration instead.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to sort loop seam clicks out numerically instead of hunting for them by ear
✦You will be able to catch the case where a repaired waveform starts clicking again after conversion to the shipping format, before your users hear it
✦You will be able to estimate the audio tokens a seam check costs by narrowing the clip to the wrap point and reading it off the duration
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
numpy is the only third-party dependency. A check that runs on every asset swap will eventually break if the dependency list keeps growing, and the standard library's wave module is enough to read a WAV.
"""Sort loop seams numerically before listening for them.Standard library plus numpy only (wave assumes 16-bit PCM WAV)."""import sys, waveimport numpy as npdef read_wav_mono(path): with wave.open(path, "rb") as w: if w.getsampwidth() != 2: raise ValueError(f"{path}: 16-bit PCM WAV only") ch, sr, n = w.getnchannels(), w.getframerate(), w.getnframes() raw = np.frombuffer(w.readframes(n), dtype="<i2").astype(np.float64) / 32768.0 if ch > 1: # fold stereo down before judging raw = raw.reshape(-1, ch).mean(axis=1) return raw, srdef seam_metrics(x, sr, edge_ms=10.0): """Normalise the seam step by how much this track normally moves. Absolute thresholds vary too much per source; the ratio holds up in practice.""" step = np.abs(np.diff(x)) p999 = float(np.percentile(step, 99.9)) # this track's largest normal move jump = abs(float(x[0]) - float(x[-1])) # how far the loop point leaps k = max(1, int(sr * edge_ms / 1000)) edge = np.concatenate([x[:k], x[-k:]]) return { "jump": jump, "ratio": jump / max(p999, 1e-12), # <- the metric that decides "p999_step": p999, "edge_rms": float(np.sqrt(np.mean(edge ** 2))), "samples": int(len(x)), }def verdict(m, ratio_threshold=1.0, edge_rms_floor=1e-3): if m["edge_rms"] < edge_rms_floor: return "SILENT_EDGE" # silent edges: no step, but check length drift instead return "SUSPECT" if m["ratio"] >= ratio_threshold else "OK"if __name__ == "__main__": for path in sys.argv[1:]: x, sr = read_wav_mono(path) m = seam_metrics(x, sr) print(f"{path:24s} {verdict(m):12s} ratio={m['ratio']:8.3f} " f"jump={m['jump']:.5f} p999={m['p999_step']:.5f} " f"edge_rms={m['edge_rms']:.5f} n={m['samples']}")
The threshold of 1.0 means "flag it when the leap at the seam is as large as the biggest move this track makes internally". There is no rigorous derivation behind it. It is the value that stopped disagreeing with my ears once I ran my own material through.
The numbers I got from my own material
I built two ten-second synthetic pads for verification. In one, the partial frequencies divide the loop length exactly. In the other, they are detuned slightly so the wrap lands mid-cycle.
Material
Verdict
ratio
jump
p999_step
Periods aligned
OK
0.16
0.00912
0.05781
Detuned
SUSPECT
4.92
0.26472
0.05382
The part worth noticing is that p999_step is nearly identical across both. The texture has not changed at all, yet ratio opens up by close to thirty times. That gap only appears because the denominator is recomputed per track.
The repair is an equal-power crossfade — fold a slice of the tail over the head and shorten the total by that much.
def crossfade_loop(x, sr, ms=30.0): """Fold the last ms into the head with equal power, trimming the total length.""" L = int(sr * ms / 1000) f = np.linspace(0, 1, L) mixed = x[:L] * np.sqrt(f) + x[-L:] * np.sqrt(1 - f) # equal-power law return np.concatenate([mixed, x[L:-L]])
Sweeping the crossfade length and re-measuring gave this:
Crossfade length
ratio
Remaining samples
5 ms
0.017
440,780
15 ms
0.395
440,339
30 ms
0.354
439,677
60 ms
0.242
438,354
120 ms
0.372
435,708
Every length lands far below 1, and longer is not monotonically better. Which means crossfade length is not a correctness question at all — it is a question of how much tail you want to keep. I spent a while looking for an optimum that was never there.
I repaired the waveform, then the shipping format undid it
This is the part I did not see coming.
The crossfaded WAV passes at ratio 0.354. I converted it to the format that actually ships in the app, decoded it back, and measured again.
Shipping format
Verdict
ratio
Samples
Delta
WAV (source)
OK
0.354
439,677
—
MP3 192 kbps (LAME)
OK
0.198
439,677
±0
AAC 192 kbps (.m4a)
SUSPECT
12.49
440,320
+643
The AAC file gained 643 samples. The source, 439,677 samples, leaves a remainder of 381 when divided by 1024. The padded length, 440,320, is exactly 430 frames of 1024. Silence had been appended out to a whole frame boundary.
Looking at the decoded tail directly, the final four hundred and fifty samples or so are pure silence. The head begins partway up at an amplitude of 0.66, so on playback the wrap leaps from 0 to 0.66. My carefully folded crossfade had been pushed past the cut, into a position nobody would ever hear.
MP3 came through untouched, because LAME writes gapless information the decoder reads to trim the padding at both ends. This sits on a different axis from the usual "lossy compression degrades the sound" conversation: the question here is whether the codec and container rewrite the length.
Test the format you ship. A passing master is not a passing artifact. I had stopped at the source file and never looked further down the chain.
I have found exactly one way to avoid this: decode the converted file and run the identical check a second time. Running the same measurement twice, mechanically, has proved more reliable for me than trying to remember how each encoder behaves.
The same shape of problem shows up when tooling changes generation. I wrote about checking first whether something you validated has been rewritten in another layer in the one line to check first when picking a Gemini model.
Hand Gemini only what survives the sieve
Some files still come out SUSPECT, and that is where audio understanding earns its place. But I changed what I send. Instead of the whole track, I build a short clip with the head and tail swapped: one second of the tail followed by one second of the head.
The centre of that two-second clip is the exact moment the loop wraps during playback. It is what a person would do by hand, and it gives the model a much smaller place to look.
import os, waveimport numpy as npfrom google import genaifrom google.genai import typesclient = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) # export GEMINI_API_KEY=YOUR_API_KEYdef build_seam_clip(x, sr, context_ms=1000.0): """Put the moment the loop wraps at the centre of a short clip.""" k = int(sr * context_ms / 1000) return np.concatenate([x[-k:], x[:k]]) # tail first, then headdef to_wav_bytes(x, sr): import io buf = io.BytesIO() with wave.open(buf, "wb") as w: w.setnchannels(1); w.setsampwidth(2); w.setframerate(sr) w.writeframes((np.clip(x, -1, 1) * 32767).astype("<i2").tobytes()) return buf.getvalue()PROMPT = ( "This clip contains only the wrap point of a looping audio track. " "The exact centre is the moment the loop repeats. " "Do you hear a click or a pop near the centre? " "If so, set audible to true and give the approximate offset from the centre in milliseconds. " "Do not comment on musical quality or mood.")def ask_seam_audible(x, sr, model="gemini-3.8-flash"): clip = build_seam_clip(x, sr) res = client.models.generate_content( model=model, contents=[ types.Part.from_bytes(data=to_wav_bytes(clip, sr), mime_type="audio/wav"), PROMPT, ], config=types.GenerateContentConfig( response_mime_type="application/json", response_schema={ "type": "object", "properties": { "audible": {"type": "boolean"}, "offset_ms": {"type": "number"}, "note": {"type": "string"}, }, "required": ["audible"], }, ), ) return res.text # JSON string; json.loads it on the caller side
Narrowing the clip does more than sharpen the answer. Audio tokens scale with duration. At the thirty-two-tokens-per-second figure given in the Gemini API audio understanding documentation, sending a ten-second track whole costs 320 tokens, while one second either side of the seam costs 64. On a three-minute bed the gap grows considerably.
The JSON schema is a scar from that first attempt. Left free-form, the model drifted back toward musical commentary. Fixing the shape of the answer in advance means that when it cannot tell, it simply returns audible: false.
How far to trust an automatically accepted verdict is a question I would rather answer together with a sampling plan. I carried the approach from sample the outputs you auto-accepted straight into this audio check.
The order I follow when swapping material now
Since going two-stage, asset swaps settled into this sequence.
Run the master WAV through seamcheck.py and confirm ratio is below 1
For anything marked SILENT_EDGE, check length and silence duration separately instead
Convert to the shipping format, decode it back, and run the same check again
Only what is still SUSPECT goes to Gemini, as two seconds around the seam
Only what comes back audible: true gets my own ears
Step three is the one that disappears most easily. Left outside the written sequence, I skip it myself every time.
On the broader stance of asking a model for evidence rather than a verdict, I tried the same thing on the image side in a provenance gate that lists evidence. The reasoning carries over to audio: keeping the final call on my side of the line makes the operation lighter, not heavier.
Measure first, then ask
If you work with looping material too, I would suggest running just one of your own tracks through seamcheck.py and looking at the ratio. Pass or fail, that number becomes the baseline for your own library.
I did feel a little foolish that two evenings of listening came down to a dozen lines of arithmetic. Still, noticing that I had confused which tool owned which layer is probably the part that keeps paying.
My thanks for staying with a story that ends in twelve lines of arithmetic.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.