Merge branch 'flow/m3te/P13.1' into m3te-plan

This commit is contained in:
swipelab
2026-06-06 09:28:41 +03:00
20 changed files with 163 additions and 211 deletions

181
README.md
View File

@@ -32,7 +32,9 @@ bash tools/run_tests.sh
### 2. iOS Simulator build + launch
Build the app for the simulator, then install/launch it on an available device
and screenshot the rendered scene (blue background + a centered orange quad).
and screenshot the rendered scene — the brighter **candy** match-3 board: a soft
pink→lavender→blue gradient background, an 8×8 grid of glossy candy gems on cream
cell tiles, and a grape candy HUD card (score / moves) above the grid.
```bash
# Build the .app bundle (sx-out/ios/M3te.app):
@@ -53,10 +55,16 @@ xcrun simctl launch booted co.swipelab.m3te
xcrun simctl io booted screenshot /tmp/m3te.png
```
The screenshot should match `goldens/p0_quad.png` (a centered orange quad over a
blue clear), modulo the status-bar clock — pixel-exact equality is not required.
A tap on the quad flips its color (orange ↔ green); see
`goldens/p0_input_before.png` / `goldens/p0_input_after.png`.
The screenshot should match `goldens/p6_idle_t0.png` (the resting candy board),
modulo the status-bar clock — pixel-exact equality is not required; compare the
board + HUD region, not the top status strip. A tap selects a cell; a swipe
between two adjacent gems commits a swap (legal swaps cascade + score, illegal
ones ping back). The deterministic capture hooks below pin a chosen scene so each
golden reproduces from a clean checkout.
> The earlier `p0_*` goldens (a pre-board orange quad over a blue clear, from the
> P0 foundation slice) were removed in P13.1: the app no longer renders that
> scene, so they could only ever mislead.
### Deterministic animation capture (P6.3)
@@ -72,14 +80,22 @@ the launched app, so prefix them on the `simctl launch`:
- `M3TE_SELECT=<cellIndex 0..63>` (= `row*8 + col`) force-selects a cell at
startup, so the selection highlight + pop can be captured without a tap.
> **Every `M3TE_*` pin is read once, at app startup.** A `simctl launch` against
> an already-running copy just foregrounds the existing process (the launch prints
> the *same* PID) and the new `SIMCTL_CHILD_*` value is ignored. So every
> multi-launch recipe below passes `--terminate-running-process` on each pinned
> launch — it kills the running copy first, so the fresh process re-reads the new
> pin. (`xcrun simctl terminate booted co.swipelab.m3te` before each relaunch does
> the same.) A *changed* PID between launches confirms the new pin took.
```bash
# Resting board (idle at rest): goldens/p6_idle_t0.png
SIMCTL_CHILD_M3TE_ANIM_TIME=0 xcrun simctl launch booted co.swipelab.m3te
SIMCTL_CHILD_M3TE_ANIM_TIME=0 xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Mid-breath idle: goldens/p6_idle_mid.png
SIMCTL_CHILD_M3TE_ANIM_TIME=1.0 xcrun simctl launch booted co.swipelab.m3te
SIMCTL_CHILD_M3TE_ANIM_TIME=1.0 xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Selection pop on cell (3,3): goldens/p6_select.png
env SIMCTL_CHILD_M3TE_ANIM_TIME=0.17 SIMCTL_CHILD_M3TE_SELECT=27 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
```
With no variable set the game runs fully live (the clock advances by
@@ -103,13 +119,13 @@ idle clock:
```bash
# Win banner + restart over the board: goldens/p7_win.png
env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Lose banner ("OUT OF MOVES") + restart: goldens/p7_lose.png
env SIMCTL_CHILD_M3TE_MOVE_LIMIT=0 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Fresh in_progress board after restart: goldens/p7_restart.png
env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_RESTART=1 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
```
While a banner is up the board freezes (only the restart button is live, per
@@ -140,10 +156,10 @@ on this seed) used to capture the escalated combo emphasis (next section).
```bash
# Punchy match burst + "+30" popup, pinned mid-clear: goldens/p6_fx_match.png
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Same match, later phase — FX fully gone over the settled board (no golden):
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=2.0 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
```
### Escalating combo emphasis (P11.2)
@@ -161,13 +177,13 @@ seed 1337, contrasted against the depth-1 single clear `M3TE_FX=3`:
```bash
# Escalated COMBO x5 + gold "+1050" + bigger burst: goldens/p11_combo_deep.png
env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Single clear for contrast — plain white "+30", no COMBO label (goldens/p6_fx_match.png):
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Deep cascade at a later phase — all combo FX gone over the settled board (no golden):
env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=3.0 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
```
The combo emphasis is purely visual and self-pruning: it never gates input
@@ -189,11 +205,140 @@ Capture it with the same P6.3 hooks — no new env var:
```bash
# Glossy candy selection on cell (3,3), pinned mid-pop: goldens/p6_select.png
env SIMCTL_CHILD_M3TE_ANIM_TIME=0.17 SIMCTL_CHILD_M3TE_SELECT=27 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Same selection at exact rest (no pop) — isolates the overlay:
env SIMCTL_CHILD_M3TE_ANIM_TIME=0 SIMCTL_CHILD_M3TE_SELECT=27 \
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
```
The selection gloss is purely visual: it never gates input (`BoardAnim.active`
owns gating) and never touches board / score / move state.
## Audio bank (P10) — final model
The SFX bank (`audio.sx`) is a purely additive layer over iOS **System Sound
Services** (AudioToolbox, reached by `#foreign` FFI exactly as the platform reaches
UIKit/Metal). Each cue is loaded once at startup into its own `SystemSoundID`;
playback is a single C call. The bank only plays when *every* cue loaded (`loaded`
gate), so a partial/failed bank mutes rather than playing a stale id. It never
reads or mutates score / board / move state — `board_view` and the frame loop just
tell it an event happened.
### Provenance — the user-provided "Triple Treat SFX" pack
The nine shipped cues are best-fit selections from the user-supplied **Triple Treat
SFX** pack (`Triple_Treat_SFX.zip`, ~30 MB, 280 Unity-style files). The pack itself
is **not committed** — only the selected, converted cues live in `assets/audio/`;
the source archive is kept outside the repo (in `~/Downloads`). Full per-cue notes
are in `assets/audio/LICENSE.txt`. The shipped game never re-synthesizes anything;
it only loads the finished WAVs.
| cue | game event | pack source (under `Triple Treat SFX/`) |
|--------------|------------------------------------|------------------------------------------|
| `swap.wav` | every committed swipe (subtle) | `Transition SFX/Swipe FX 1-RCM.wav` |
| `match.wav` | first clear of a legal move (pop) | `Pop:Bubble SFX/Pop FX 5-RCM.wav` |
| `combo1.wav` | cascade round 1 (dullest) | `Match SFX/Match FX 2-RCM.wav` (~1.7 kHz)|
| `combo2.wav` | cascade round 2 | `Match SFX/Match FX 4-RCM.wav` (~2.1 kHz)|
| `combo3.wav` | cascade round 3 | `Match SFX/Match FX 6-RCM.wav` (~3.2 kHz)|
| `combo4.wav` | cascade round 4 | `Match SFX/Match FX 7-RCM.wav` (~4.7 kHz)|
| `combo5.wav` | cascade round ≥5 (brightest) | `Match SFX/Match FX 3-RCM.wav` (~6.8 kHz)|
| `win.wav` | level won (stinger) | `Success:Power-Up SFX/Power Up FX 1-RCM.wav` |
| `lose.wav` | level lost (stinger) | `Fail SFX/Fail FX 2-RCM.wav` |
The Match-FX set does not cleanly pitch-ascend, so `combo1..5` are ordered by
ascending **spectral brightness** (centroid 1.68 < 2.09 < 3.18 < 4.70 < 6.77 kHz)
so a deeper cascade reads as more exciting.
### Per-round ascending cascade
The cascade plays **one ascending cue per cascade round** — round 1 → `combo1`,
round 2 → `combo2`, … clamped at `combo5`*not* a single end-of-move sound. Each
cue fires on the move's animation timeline, edge-triggered as that round's clear
begins: the frame loop (`main.sx`) diffs `cascade_rounds_started(elapsed, rounds)`
against `BoardAnim.cascade_fired` and plays the next combo cue per newly-cleared
round (`sfx_cascade`). The depth→index clamp (`cascade_cue_index`: depth ≤ 1 → 0,
depth ≥ 5 → 4) is pure + headless, snapshot-tested by `tests/cascade_cue.sx` and
`tests/cascade_rounds.sx`. A single match (one round) plays only the `match` pop —
no combo cue. The `swap` cue plays for any committed gesture (legal or the reverted
ping-back); a legal move adds the `match` pop on its first clearing round; the
`win`/`lose` stinger fires once, edge-triggered, as the banner comes up.
### Format / level spec
Every cue is delivered in exactly the form System Sound Services loads directly:
**WAVE / mono / 44100 Hz / signed-16-bit PCM**. The pack sources are 24-bit /
48 kHz / stereo; each was down-mixed to mono, trimmed to its punchy window, eased
in with a short fade, rounded out with a cosine fade-out tail, and **peak-normalized
to a gentle 15 dBFS** (the user rejected aggressive SFX twice). The candy character
of the pack is preserved — the cues are not re-synthesized.
### Capturing the cue ordering
Every play is `NSLog`'d, so a playthrough's cue order is readable from the device
log. The deep cascade (`M3TE_FX=11`, depth-5 on seed 1337) must be launched **live**
(no `M3TE_ANIM_TIME` pin) so the timeline advances and the per-round cues fire in
sequence:
```bash
# Deep cascade, live — fires combo1..combo5 one per round on the timeline:
SIMCTL_CHILD_M3TE_FX=11 xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
xcrun simctl spawn booted log show --last 20s \
--predicate 'eventMessage CONTAINS "[sx] audio"' --style compact
# Win / lose stingers (also live so the banner edge-triggers the cue). Each pinned
# launch passes --terminate-running-process: the M3TE_* pins are read only at
# startup, so relaunching a still-running copy reuses its PID and the new pin is
# ignored — without it the lose launch reuses the win process and only `cue win`
# ever prints. A changed PID between the two launches confirms each pin took:
SIMCTL_CHILD_M3TE_TARGET=0 xcrun simctl launch --terminate-running-process booted co.swipelab.m3te # cue win
SIMCTL_CHILD_M3TE_MOVE_LIMIT=0 xcrun simctl launch --terminate-running-process booted co.swipelab.m3te # cue lose
```
## Asset regeneration
The shipped game only loads finished assets; the steps below rebuild them and are
never run at play time. Refresh any affected `goldens/` after an art change.
### Audio (the SFX bank)
The shipped cues are **not synthesized** — each is a real **Triple Treat SFX** pack
clip selected per game event (per-cue source in `assets/audio/LICENSE.txt`). To
re-derive a cue from the pack, take its source file and apply the same DSP the
shipped bank used: down-mix to mono, trim to the punchy onset window (≤ ~600 ms),
ease in with a short fade, round out with a cosine fade-out tail, peak-normalize to
a gentle **15 dBFS**, then re-wrap to the canonical container with `afconvert`
(`afconvert -f WAVE -d LEI16@44100 -c 1 in.wav out.wav` → WAVE / `LEI16` / 44100 /
mono). The combo ladder is five real **Match FX** cues ordered by ascending spectral
brightness (P10.9, `combo1``combo5`); the cascade plays one cue per round on the
animation timeline (P10.10). Verify the result read-only with `tools/measure_pitch.py`
(it never writes a WAV) — it prints each clip's dominant frequency, and `clear.wav`'s
~784 Hz CC0 reference is the documented baseline. The 30 MB pack and its `.meta` /
`__MACOSX` cruft are not committed.
> There is intentionally **no procedural-synthesis regeneration script**: the cues
> are the curated pack clips, so the original P10.1 note-frequency synthesizer
> (`tools/synth_audio.py`) was removed — running it would have clobbered the curated
> WAVs with synthetic audio.
### Image art
Real art is produced with codex's built-in **`imagegen`** tool through `codex exec`,
then `sips`-normalized to each asset's exact dimensions and source format:
| asset | dims (px) | format | role |
|-----------------------------|-----------|--------|-----------------------------------|
| `assets/board/background.png` | 863×1822 | PNG | full-view candy gradient backdrop |
| `assets/board/cell.png` | 128×128 | PNG | one grid cell tile |
| `assets/gems/gems.png` | 768×128 | PNG | 6 gem columns (a gem's UV column = its index) |
| `assets/fx/particle.png` | 256×256 | PNG | soft match-burst sprite (tinted per gem) |
```bash
# 1. Generate (codex's imagegen tool, driven non-interactively):
codex exec "use the imagegen tool to render <prompt for the asset>"
# 2. Normalize to the exact per-asset dims + format, e.g. the gem sheet:
sips -z 128 768 --setProperty format png <generated>.png --out assets/gems/gems.png
```
After any art change, re-capture the affected goldens with the deterministic hooks
above (`M3TE_ANIM_TIME` / `M3TE_SELECT` / `M3TE_FX` / `M3TE_TARGET` /
`M3TE_MOVE_LIMIT` / `M3TE_RESTART`) and state per golden whether it was refreshed,
left unchanged, or removed.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@@ -1,193 +0,0 @@
#!/usr/bin/env python3
"""Synthesize the m3te candy-vibe SFX bank (P10.1).
BUILD-TIME TOOL ONLY. The shipped game never runs this; it only loads the
finished WAVs this script writes under assets/audio/. Pure Python stdlib
synthesis (no numpy) -> standard 16-bit PCM WAV -> afconvert into the exact
canonical iOS System-Sound format clear.wav already uses (mono, 44100 Hz,
LEI16, WAVE_FORMAT_EXTENSIBLE).
Timbre: a bright glossy bell — fundamental plus a few decaying partials and a
slightly detuned shimmer top, a fast attack, exponential decay, and a tiny
upward pitch blip at onset for the "candy pop" feel. Every cue sits well above
clear.wav's ~784 Hz fundamental. combo1..combo5 climb a pentatonic run.
Usage: python3 tools/synth_audio.py # writes the whole bank
python3 tools/synth_audio.py --keep-tmp # leave the intermediate PCM
"""
import math
import os
import struct
import subprocess
import sys
import tempfile
import wave
SR = 44100
# Equal-tempered note frequencies (A4 = 440 Hz).
C6, D6, E6, G6, A6, C7 = 1046.50, 1174.66, 1318.51, 1567.98, 1760.00, 2093.00
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "audio")
# A glossy candy bell: fundamental + gently decaying harmonics + a slightly
# detuned high partial for shimmer. Kept clean (no harsh odd-only stack).
GLOSSY = [(1.0, 1.00), (2.0, 0.34), (3.0, 0.13), (4.02, 0.06), (6.01, 0.03)]
# Softer/darker partial set for the gentle "lose" stinger.
GENTLE = [(1.0, 1.00), (2.0, 0.22), (3.0, 0.07)]
def note(freq, dur, partials=GLOSSY, decay=0.18, blip=0.06, blip_tau=0.012,
attack=0.004):
"""One glossy bell note as a list of floats (mono, length dur*SR).
decay: exponential amplitude time-constant (s).
blip: fractional upward pitch offset at onset, settling with blip_tau.
"""
n = int(dur * SR)
out = [0.0] * n
phases = [0.0] * len(partials)
for i in range(n):
t = i / SR
# Onset pitch blip: starts ~blip above target, settles fast.
fmul = 1.0 + blip * math.exp(-t / blip_tau)
amp = math.exp(-t / decay)
if t < attack: # linear attack from 0 (no click)
amp *= t / attack
s = 0.0
for k, (ratio, level) in enumerate(partials):
phases[k] += 2.0 * math.pi * freq * ratio * fmul / SR
s += level * math.sin(phases[k])
out[i] = amp * s
return out
def mix(layers):
"""Overlay (freq, start_s, samples) layers onto one buffer."""
end = max(int(start * SR) + len(buf) for _, start, buf in layers)
out = [0.0] * end
for _, start, buf in layers:
off = int(start * SR)
for i, v in enumerate(buf):
out[off + i] += v
return out
def finalize(samples, peak=0.89, fade=0.008):
"""Soft-clip for warmth, normalize, and fade the tail to avoid a click."""
samples = [math.tanh(1.3 * s) for s in samples]
m = max(1e-9, max(abs(s) for s in samples))
g = peak / m
samples = [s * g for s in samples]
fn = int(fade * SR)
n = len(samples)
for i in range(min(fn, n)): # linear tail fade
samples[n - 1 - i] *= i / fn
return samples
def write_pcm16(path, samples):
with wave.open(path, "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(SR)
frames = bytearray()
for s in samples:
v = int(max(-1.0, min(1.0, s)) * 32767.0)
frames += struct.pack("<h", v)
w.writeframes(bytes(frames))
def to_canonical(tmp_pcm, out_wav):
# Match clear.wav exactly: WAVE / LEI16 / 44100 / mono via afconvert.
subprocess.run(
["afconvert", "-f", "WAVE", "-d", "LEI16@44100", "-c", "1",
tmp_pcm, out_wav],
check=True,
)
# --- the bank -------------------------------------------------------------
def glide_note(f0, f1, dur, partials=GLOSSY, decay=0.12, attack=0.004):
"""A note whose pitch glides f0 -> f1 across its length (whoosh)."""
n = int(dur * SR)
out = [0.0] * n
phases = [0.0] * len(partials)
for i in range(n):
t = i / SR
frac = t / dur
freq = f0 * (1.0 - frac) + f1 * frac
amp = math.exp(-t / decay)
if t < attack:
amp *= t / attack
s = 0.0
for k, (ratio, level) in enumerate(partials):
phases[k] += 2.0 * math.pi * freq * ratio / SR
s += level * math.sin(phases[k])
out[i] = amp * s
return out
def build_swap():
# Soft glossy tick/whoosh: short, gentle attack, a downward pitch glide.
return finalize(glide_note(D6, C6 * 0.94, 0.13, decay=0.06, attack=0.006,
partials=[(1.0, 1.0), (2.0, 0.20), (3.0, 0.07)]),
peak=0.80)
def build_match():
# Bright candy pop: punchy E6 with a strong onset blip.
return finalize(note(E6, 0.17, decay=0.10, blip=0.10, blip_tau=0.010))
def build_combo(freq):
# One step of the ascending chime run: glossy, a touch of ring.
return finalize(note(freq, 0.22, decay=0.16, blip=0.05, blip_tau=0.010))
def build_win():
# Triumphant ascending arpeggio C6-E6-G6-C7, notes ring together.
notes = [(C6, 0.00), (E6, 0.07), (G6, 0.14), (C7, 0.21)]
layers = [(f, t, note(f, 0.30, decay=0.20, blip=0.04)) for f, t in notes]
return finalize(mix(layers), peak=0.92)
def build_lose():
# Gentle descending stinger G6-E6-C6, soft and dark.
notes = [(G6, 0.00), (E6, 0.11), (C6, 0.22)]
layers = [(f, t, note(f, 0.28, partials=GENTLE, decay=0.18, blip=0.0))
for f, t in notes]
return finalize(mix(layers), peak=0.72)
BANK = {
"swap": build_swap,
"match": build_match,
"combo1": lambda: build_combo(C6),
"combo2": lambda: build_combo(D6),
"combo3": lambda: build_combo(E6),
"combo4": lambda: build_combo(G6),
"combo5": lambda: build_combo(A6),
"win": build_win,
"lose": build_lose,
}
def main():
keep = "--keep-tmp" in sys.argv[1:]
out_dir = os.path.normpath(OUT_DIR)
for name, build in BANK.items():
samples = build()
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
tmp = tf.name
write_pcm16(tmp, samples)
out = os.path.join(out_dir, name + ".wav")
to_canonical(tmp, out)
if not keep:
os.remove(tmp)
print("wrote %s (%.3f s)" % (out, len(samples) / SR))
if __name__ == "__main__":
main()