Reflex reads and writes files at two levels:
File::Open and File::SaveSystem::FileHandle, with the File:: helpersStart at the top. Drop down to a handle only when you need more control.
FileHandle::Create plus ReadBytes, ReadLine, ReadValue.To make the file calls concrete, we will build a small waveform. Point it at an audio file and it draws the shape of the sound: peaks where the signal is loud, flat stretches where it is quiet.
Open the source audio and decode it into samples. File::Open reads the whole file into a Data::Archive, a byte buffer, and an app-side decoder turns those bytes into one channel of Float32 samples in the range [-1, 1]. That is the equivalent of Web Audio decodeAudioData followed by getChannelData.
auto bytes = File::Open(path);
if (!bytes) return false; // missing or unreadable
Array<Float32> samples;
UInt rate = 0;
LoadWav(bytes.GetData(), bytes.GetSize(), samples, rate); // WAV bytes -> Float32 [-1,1]
File::Open does not throw. On a missing or unreadable file it returns an empty Data::Archive, which is falsy, so if (!bytes) is the whole guard.GetData() and GetSize(). What decodes them is yours: LoadWav here parses uncompressed WAV and returns false on bytes it cannot read; swap in your own codec for MP3, FLAC, or OGG. The WaveformViewer demo can also synthesise its samples, so it ships with no external file.File::Open takes a WString. Paths often arrive as a WString::View, so wrap them at the call: File::Open(WString(view)).That same truthiness gives you a compact form when you use the bytes right away, binding and testing in one step:
if (auto request = File::Open(request_path))
{
return Data::DecodePropertySet(Data::kJsonFormat, request);
}
// else: file absent or unreadable, fall back
One caveat. A zero-byte file is also empty, so empty means no bytes, not strictly an error. To tell an empty file from a missing one, gate on File::Exists(path) first.
With samples in hand, persist them. Build a Data::PropertySet, the tree Reflex uses for any structured data, then encode it as JSON and hand the bytes to File::Save in one call.
Data::PropertySet json;
auto keymap = Data::AcquireKeyMap(json);
auto rate_key = Data::RegisterKey(keymap, "sample_rate");
auto sample_key = Data::RegisterKey(keymap, "sample");
Data::SetInt64(json, rate_key, m_sample_rate);
auto channel = Data::AcquirePropertySetArray(json, Data::RegisterKey(keymap, "samples"));
for (UInt i = 0; i < m_samples.GetSize(); ++i)
{
auto node = Data::AddPropertySet(channel);
Data::SetFloat32(node, sample_key, m_samples[i]);
}
File::Save(WString(path), Data::EncodePropertySet(Data::kJsonFormat, json));
Data::EncodePropertySet(Data::kJsonFormat, json) turns the tree into bytes; File::Save writes them. The save call is the same one you would use for a raw buffer, only the source differs.File::Save directly. JSON earns its place when you want the file human readable or carrying metadata like the sample rate.Reading is the mirror image. File::Open feeds its bytes straight into Data::DecodePropertySet, and you pull the values back out by key.
auto json = Data::DecodePropertySet(Data::kJsonFormat, File::Open(WString(path)));
m_sample_rate = UInt(Data::GetInt64(json, "sample_rate"));
auto channel = Data::GetPropertySetArray(json, "samples");
m_samples.Clear();
for (auto& i : channel)
{
m_samples.Push(Data::GetFloat32(*i, "sample"));
}
File::Open nests inside the decode call, so a missing file decodes to an empty set and the getters return safe defaults. No cast checks, no exceptions.GetPropertySetArray is safe to iterate even when the key is absent, so a malformed file simply comes back as no samples.Whatever the format, the shape is the same. Save is build then File::Save; read is File::Open then decode. JSON rides on top of that byte round-trip, it does not replace it.
A waveform has far more samples than the view has pixels, so first reduce each column of samples to a min and a max, then paint one bar per column.
Reduce. This is the browser computePeaks, one interleaved min, max pair per column:
void ComputePeaks(const Float32* samples, UInt count, UInt spp, Array<Float32>& peaks)
{
peaks.Clear();
for (UInt i = 0; i < count; i += spp)
{
Float32 mn = 1.0f;
Float32 mx = -1.0f;
UInt end = Min(i + spp, count);
for (UInt j = i; j < end; ++j)
{
Float32 v = samples[j];
if (v < mn) mn = v;
if (v > mx) mx = v;
}
peaks.Push(mn); // interleaved: one (min, max) pair per column
peaks.Push(mx);
}
}
Then draw. Reflex assembles the canvas once through GLX::SetColourCanvas, exactly where the browser looped over ctx.fillRect:
GLX::SetColourCanvas(*this, {}, [this](GLX::ColourCanvasContext& ctx)
{
UInt columns = m_peaks.GetSize() / 2;
Float32 mid = ctx.size.h * 0.5f;
Float32 step = ctx.size.w / Float32(columns);
const GLX::Colour wave = { 0.20f, 0.80f, 0.75f, 1.0f }; // teal
for (UInt x = 0; x < columns; ++x)
{
Float32 mn = m_peaks[x * 2];
Float32 mx = m_peaks[x * 2 + 1];
Float32 top = mid + mn * mid; // mn <= 0, so the bar starts above centre
Float32 height = (mx - mn) * mid;
GLX::AddRectFill(ctx.output, wave, { { Float32(x) * step, top }, { step, height } });
}
});
Absolute Windows paths need a leading slash, and forward slashes or escaped backslashes:
"/C:/Users/Name/file.txt" OK
"/C:\\Users\\Name\\file.txt" OK
"/C:\Users\Name\file.txt" FAILS (unescaped backslashes)
Files bundled with your app use the :res: prefix and read through the same call:
Data::Archive style = File::Open(L":res:MyApp/styles.glx");
For incremental reading, create a handle:
auto fh = System::FileHandle::Create(L"/C:/data/log.txt", System::FileHandle::kModeRead);
kModeRead is the default, so it can be omitted. The other modes are kModeOverwrite and kModeAppend, for writing through the same handle API.
Create is [[nodiscard]], so check the returned handle. A null ref (the FileHandle::null sentinel) means the open failed. From there every call reports its own result rather than throwing:
| Call | Failure signal |
|---|---|
Read(ptr, max) | bytes read; short or 0 is EOF or error |
Write(ptr, size) | bytes written |
Truncate(), Flush(commit) | false on failure |
File::ReadLine(fh, out) | false at EOF or failure |
File::Save, Copy, Delete, Rename, MakeDirectory | false on failure |
Data::Archive all = File::ReadBytes(fh); // everything from the current position
Data::Archive chunk = File::ReadBytes(fh, 256); // next 256 bytes
Data::Archive ahead = File::Peek(fh, 4); // read 4 bytes WITHOUT advancing
UInt64 left = File::GetRemainder(fh); // bytes between position and end
Peek leaves the read position untouched. Reach for it when you need to sniff a file header (magic bytes) before deciding how to parse the rest.
CString aline;
while (File::ReadLine(fh, aline)) { /* aline decoded as ASCII */ }
WString wline;
while (File::ReadLine(fh, wline)) { /* wline decoded from UTF-8 */ }
Both overloads return false when there is nothing left, so they drive a while loop cleanly. Pick the WString overload for any text that may contain non-ASCII characters; it decodes UTF-8 for you. File::WriteLine mirrors both for the write direction and appends the newline itself.
UInt32 magic = File::ReadValue<UInt32>(fh); // reads sizeof(UInt32) bytes
Header h;
if (File::ReadValue(fh, h)) { /* h filled, false on a short read */ }
Only raw-packable types compile; the header enforces this with a static assert. File::WriteValue mirrors it for writing.
UInt64 size = fh->GetSize();
fh->SetPosition(0); // rewind
UInt64 pos = fh->GetPosition();