Skip to content

Uploads converted for the pedal: mark them on purpose, and say what changed - #153

Merged
AliceLafox merged 17 commits into
mainfrom
feat/upload-conversion-mark
Oct 17, 2026
Merged

AliceLafox merged 17 commits into
mainfrom
feat/upload-conversion-mark

Conversation

@AliceLafox

Copy link
Copy Markdown
Member

Closes #139.

A file the pedal cannot take as-is was rebuilt into import-tmp/<stem>-pedal.wav, and push named the file on the card after the temp file it was handed, so the -pedal suffix reached the card by accident. The mark is worth keeping (the bytes in the slot are not the bytes of the player's file), so it becomes a decision.

  • Setting (Settings → Import): "Mark uploads converted for the pedal with", default -pedal; clearing it turns the mark off. Stored beside the other upload preferences (app/ImportPrefs.h, one reader and writer for the dialog and the push job; read when the player acts, as the normalize preference already was).
  • The name on the card is decided at the push (app/UploadMark.h, PushOptions::landedName): a rebuilt upload lands as <stem><mark>.wav (an imported song.mp3 becomes song-pedal.wav), an untouched one keeps its name, and a file whose header alone was repacked (an extensible float32 DAW export, every sample intact) lands as <stem>.wav with no mark. The conversion goes to a directory per job that dies with the job, success or throw.
  • The mark applies only when the samples changed: rate, encoding or channels differ from the pedal's, or a gain was applied, or the file was decoded by something other than JUCE's own WAV/AIFF/FLAC readers (CoreAudio on macOS reports facts that are the decoder's, not the file's).
  • Say what changed: the push toast, the history row and the operations log carry "48000 Hz, 24-bit, mono → 44100 Hz, 32-bit float, stereo" (only what differs), joined with the normalization sentence when both apply.
  • Refused before any byte moves (core/include/loopercat/FatName.hpp, assertLandedName): a name the card or a host could not hold (separators, FAT-reserved and control characters, more than 238 UTF-16 units so the Windows path fits MAX_PATH, Windows device names including the superscript digits, names the card sweep deletes, names starting with a dot). Before this, a long name passed the checks, push archived and removed the old take, and the write failed, leaving the slot empty.
  • Malformed WAVs are refused, not silently repaired (wav::riffFault): two data chunks, a cut data chunk, a header never finalised. A cut metadata trailer after complete audio still imports.
  • A pass-through WAV whose name does not end in .wav is now refused ("must end in .wav") instead of landing unindexable.

Docs: the guide's "Moving audio" section explains the mark and where to turn it off.

Tests: upload_mark_tests 77, fat_name_tests 108, import_prefs_tests 28, wav_tests 90, wav_import_tests 230, commands_tests 1630 (the landed name on the card, every refusal on an empty and an occupied slot with the card byte-identical afterwards), slot_story_tests 84, mp3_import_tests 27. Non-GUI ctest 59/59; the six GUI harnesses 6/6 locally.

Follow-ups noted, not in this PR: a startup sweep of import-tmp after a crash (a failed delete is logged); push still archives and removes the old take before writing the new one, so a write failure the checks cannot foresee (disk full) leaves the slot empty; the Settings field has no automated test (no settings harness); Windows narrow-path handling of non-ASCII names is pre-existing and to be tried on the Windows box.

push named the file on the card after whatever path it was handed, so the
working name of a conversion's temp file travelled to the card by accident.
PushOptions now carries an optional landed name; the core checks it before a
byte moves — a bare file name, something in front of .wav, no separator in
either direction — and refuses otherwise with the slot and the archive
untouched. Absent, the handed file's own name, as before.
…eferences

The suffix on a rebuilt upload becomes a decision: uploadmark::landedName
gives <source stem><suffix>.wav for a conversion and leaves an untouched
file's name alone, and a suffix with a character a FAT name cannot hold is
refused by name rather than stripped. The default stays -pedal, the spelling
existing cards already carry.

ImportPrefs gathers the three upload preferences — the normalize switch, its
target and the mark — with one reader and one writer over the PropertiesFile,
so the dialog, the push job and a test see the same keys and defaults. A mark
cleared to nothing stays nothing: the key is present, its value empty.
wavimport::prepare wrote <stem>-pedal.wav into import-tmp, renamed around a
clash, and left the caller to delete the file by hand on both exits of the
push. The conversion now goes to a directory of its own under import-tmp,
owned by the Prepared through a RAII handle: gone when the job returns and
when it throws alike, and two uploads with the same stem in one queue never
meet.

Prepared also reports the source's sample rate, sample format and channel
count. A compressed source names its codec, because the float32 our MP3
reader hands out is the decoder's, not the file's. describeConversion says
only what differs from the pedal's 44100 Hz, 32-bit float, stereo — so a
loudness-only rewrite of a pedal-ready file has no sentence at all.
…nged

The push job reads the upload preferences when the player acts, names the
file on the card through the mark rule, and hands that name to push instead
of inheriting the temp file's. The note the toast and the history row carry
opens with the conversion's facts, joined to the normalization sentence when
both apply, and the operations log line says the landed name when it differs
from the source's.

Settings -> Import gains the mark field under the normalize switch. A
character a FAT name cannot hold snaps the field back to the stored mark and
says why beneath it; nothing is stripped on the way in.
Why a rebuilt upload lands as <name>-pedal.wav, what the push report says
about it, and where the suffix lives.
describeConversion joined its two sides with an ASCII "->" while
describeNormalize, which shares the same note, draws U+2192. A push that
was both rebuilt and levelled read with two different arrows in one
sentence. The conversion now uses the same escape, the tests pin the
sentences with it, and the guide's example matches what the toast shows.
…old take moves

push archives and removes the old take before it writes the new one, so a
name the card refuses at the write leaves the slot folder empty under a
config that says it holds audio. The up-front check had no length rule, and
a converted upload's <stem><mark>.wav could pass it at 256 UTF-16 units and
fail on the card (review of #139: a 252-character stem with force, a
247-character stem through the whole job, a 260-character mark).

FatName.hpp now owns what a file name on the card may be — FAT's reserved
characters and control range, the 255-unit limit counted in UTF-16 units
from the UTF-8, and the device names Windows will not open, judged before the
first dot as Windows judges them — so the core and the app check the same
rules. assertLandedName refuses all of it before a byte moves, and with it a
name the card's own sweep would delete ("._take.wav" was reported as pushed
and then swept) and a name starting with a dot. The reproductions leave the
card byte-identical and the old take in place.
A float DAW export wearing a WAVE_FORMAT_EXTENSIBLE header fails the pedal's
strict gate and goes through the converter, so it landed as <stem>-pedal.wav
with an empty sentence although no sample moved. "Rebuilt" is now a fact
Prepared reports: the rate, sample format or channel count differed from the
pedal's, or a gain went in. The mark keys on it; a header-only rewrite lands
as <stem>.wav — the extension still changed on purpose — with no mark and no
sentence, and its samples equal the source's bit for bit. `converted` keeps
meaning what it did: the file lives in the job's directory.

landedName takes what happened to the audio (untouched, repackaged, rebuilt)
and judges the mark only where it is used, so a mark hand-edited into the
settings file no longer stops an untouched upload. A name it builds over the
FAT limit is refused with the name it would have been, never trimmed, and
assertSuffix carries a named cap so the Settings field refuses a mark no name
could carry. The field trims: a mark of only spaces is the empty mark the
hint promises.

FLAC keeps its samples' width and JUCE's reader reports it, so a FLAC source
reads "24-bit" like PCM; only MP3 and Ogg Vorbis name their codec. A job
directory that will not delete leaves a line in the operations log — the
only trace a leftover has until something sweeps import-tmp. The ImportPrefs
comment now says what the code does: the preferences are read as the job is
queued and travel with it.
The guide says the push toast and the history row spell out what changed,
but the push row drew only the take's name, length and tempo; the job's note
was stored and never shown. The row now draws the note's sentences after the
numbers — the rebuild's facts, the normalization's — leaving the note-length
fact to the place it already had at the end.
…ice names

FAT's 255 units was not the binding limit: the card is read on Windows,
where a path is capped at MAX_PATH — 260 characters, terminator included —
unless the host opted into long paths, which neither JUCE nor this app
declares. The card folder "E:\ROLAND\WAVE\001_1\" is 21 characters, so a
name of 239 to 255 units passed every check and brought back the outcome the
last round fixed: archive, remove, fail at the write. The cap is now 238 on
every platform (the same card meets every host), with the arithmetic in
FatName.hpp; 238 passes, 239 is refused, before a byte moves.

Windows also reserves COM and LPT with the superscript digits 1-3
(U+00B9, U+00B2, U+00B3); isDeviceName now takes those as the digit.
readWavInfo refuses a truncated file along with every other malformed one,
so an importer could not tell "cut short" from "not a WAV" without reading
the message. isTruncatedRiff says that one thing — the chunks claim more
bytes than the buffer holds — so the import can refuse to pad it.
"Repackaged" was decided on reader facts that can lie. On macOS
registerBasicFormats adds CoreAudio, which takes any WAV JUCE's own reader
refuses — float64, mu-law, anything over 32 bits — and always reports
32-bit float, so a float64 export read as "nothing differs" and landed
unmarked, with no sentence, with every sample re-quantised. Positive
evidence only, now: a source is repackaged only when a reader that reads
samples as they lie (JUCE's WAV, AIFF and FLAC readers) reports the pedal's
own shape and no gain went in; anything a decoder made is rebuilt, and the
sentence says what it can — "decoded by the system → 32-bit float", or the
codec. CoreAudio stays registered: dropping it would lose AAC and M4A.

The readers are told apart by the name the READER gives itself, not by the
format object that was asked: WavAudioFormat hands an Ogg stream inside a
WAV to the Ogg reader, which the old dynamic_cast missed ("16-bit float →
32-bit float"). The mp3 reader's name is a constant of its format now.

A WAV whose header claims more frames than the file holds is refused with
its name, never padded: JUCE fills the missing frames with silence, and the
pedal would have got a loop twice as long as its audio under the player's
own name.
The mark's cap derives from the card's limit, so it follows the 238 of
FatName.hpp: 233 units at most, leaving the extension and one character of
name. A whitespace-only mark hand-edited into the settings file still landed
a "song .wav"; importprefs::read trims it as the Settings field does, so
"only spaces is the empty mark" holds whichever hand wrote the file.
HistoryRecorder::finish stores a failed operation's error as its note, and
the push row had started drawing every note as the job's own words — a
refused push read "take.wav - slot 9 already has audio". story::Facts
carries the operation's status now, and the sentences are drawn only for
one that finished; a failed or interrupted row keeps the shape it had.
isTruncatedRiff flagged any chunk past the end and said nothing else, so a
complete recording with a cut LIST trailer read as "cut short" alongside a
data chunk missing half its bytes, and a header a writer never finalised
(data size 0xFFFFFFFF) read as claiming four gigabytes. riffFault names the
fault as a clause for the importer: a data chunk short of its claim by how
many bytes, a size never set, two data chunks, two fmt chunks, a short fmt
chunk, no data or no fmt chunk. A metadata chunk cut off after a complete
data chunk costs no sample and is no fault. The fmt fields themselves are
shape, not structure: an extensible tag is for the converter and the gate.
prepare took every refusal of the core's gate as a reason to convert, so a
WAV with two data chunks — refused by the core as malformed — went to
JUCE's reader, which kept the last chunk and dropped the first, and the
file landed unmarked under the player's own name with 88200 frames gone
(third review of #139). A structural fault is now refused with the fault
named, before any reader sees the file; what the gate then refuses is the
header's shape, and that is the converter's job. A recording whose cut
trailer is its only flaw is imported as a repack, every sample intact; a
data chunk short of its claim and a header never finalised are refused by
those names.
importprefs::read trimmed a stored mark, so " pedal" read as "pedal" —
against the rule UploadMark.h states, that a rewritten mark is a different
mark. One rule now, markAsTyped, shared by the Settings field and the
reader: a mark of only whitespace is the empty mark the hint promises, and
every other mark keeps its spaces before, inside and after, as typed; the
field writes back what it accepted.
@AliceLafox
AliceLafox merged commit d71ca6a into main Oct 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Uploads converted for the pedal: mark them on purpose, and say what changed

1 participant