Custom File System
This guide explains how to implement a custom FileSystem for Amplitude. A custom file system allows you to load audio assets from non-standard sources such as network streams, encrypted archives, or proprietary package formats.
Overview¶
Amplitude's FileSystem abstraction decouples the engine from platform-specific I/O. The engine requests files through this interface, and your implementation handles the actual reading.
To create a custom file system, you need two classes:
- FileSystem — Manages paths, existence checks, and file opening.
- File — Represents an open file and handles read/write/seek operations.
Step 1: Implement the File Class¶
Create a File subclass that wraps your underlying storage:
// NetworkFile.h
#pragma once
#include <SparkyStudios/Audio/Amplitude/Amplitude.h>
using namespace SparkyStudios::Audio::Amplitude;
class NetworkFile final : public File
{
public:
explicit NetworkFile(const AmOsString& url);
~NetworkFile() override;
AmSize Read(AmUInt8Buffer buffer, AmSize bytes) const override;
AmSize Write(AmConstUInt8Buffer buffer, AmSize bytes) override;
void Seek(AmInt64 offset, eFileSeekOrigin origin) override;
AmSize Length() const override;
AmSize Position() const override;
bool Eof() const override;
void Close() override;
bool IsValid() const override;
AmOsString GetPath() const override;
private:
AmOsString _url;
std::vector<AmUInt8> _data;
mutable AmSize _position;
bool _valid;
};
Implementation:
// NetworkFile.cpp
#include "NetworkFile.h"
NetworkFile::NetworkFile(const AmOsString& url)
: _url(url)
, _position(0)
, _valid(false)
{
// Fetch the file from the network (synchronous for simplicity)
_data = DownloadFromNetwork(url);
_valid = !_data.empty();
}
NetworkFile::~NetworkFile()
{
Close();
}
AmSize NetworkFile::Read(AmUInt8Buffer buffer, AmSize bytes) const
{
const AmSize remaining = _data.size() - _position;
const AmSize toRead = AM_MIN(bytes, remaining);
std::memcpy(buffer, _data.data() + _position, toRead);
_position += toRead;
return toRead;
}
AmSize NetworkFile::Write(AmConstUInt8Buffer buffer, AmSize bytes)
{
// Read-only file system
return 0;
}
void NetworkFile::Seek(AmInt64 offset, eFileSeekOrigin origin)
{
AmInt64 newPos = 0;
switch (origin)
{
case eFileSeekOrigin_Start:
newPos = offset;
break;
case eFileSeekOrigin_Current:
newPos = static_cast<AmInt64>(_position) + offset;
break;
case eFileSeekOrigin_End:
newPos = static_cast<AmInt64>(_data.size()) + offset;
break;
}
if (newPos >= 0 && newPos <= static_cast<AmInt64>(_data.size()))
_position = static_cast<AmSize>(newPos);
}
AmSize NetworkFile::Length() const
{
return _data.size();
}
AmSize NetworkFile::Position() const
{
return _position;
}
bool NetworkFile::Eof() const
{
return _position >= _data.size();
}
void NetworkFile::Close()
{
_data.clear();
_valid = false;
}
bool NetworkFile::IsValid() const
{
return _valid;
}
AmOsString NetworkFile::GetPath() const
{
return _url;
}
Step 2: Implement the FileSystem Class¶
// NetworkFileSystem.h
#pragma once
#include "NetworkFile.h"
class NetworkFileSystem final : public FileSystem
{
public:
NetworkFileSystem();
~NetworkFileSystem() override;
void SetBasePath(const AmOsString& basePath) override;
const AmOsString& GetBasePath() const override;
AmOsString ResolvePath(const AmOsString& path) const override;
bool Exists(const AmOsString& path) const override;
bool IsDirectory(const AmOsString& path) const override;
AmOsString Join(const std::vector<AmOsString>& parts) const override;
std::shared_ptr<File> OpenFile(const AmOsString& path, eFileOpenMode mode) const override;
void StartOpenFileSystem() override;
bool TryFinalizeOpenFileSystem() override;
void StartCloseFileSystem() override;
bool TryFinalizeCloseFileSystem() override;
private:
AmOsString _basePath;
};
Step 3: Register the File System¶
#include "NetworkFileSystem.h"
int main()
{
MemoryManager::Initialize();
// Create and open the custom file system
auto fs = std::make_shared<NetworkFileSystem>();
fs->SetBasePath(AM_OS_STRING("https://assets.mygame.com/audio/"));
fs->StartOpenFileSystem();
while (!fs->TryFinalizeOpenFileSystem())
Thread::Sleep(1);
// Set it as the engine's file system
amEngine->SetFileSystem(fs);
// Continue with engine initialization...
}
Async File Operations¶
Amplitude supports asynchronous file system open/close to prevent blocking the main thread:
// Initiate the open operation
fs->StartOpenFileSystem();
// Poll until complete
while (!fs->TryFinalizeOpenFileSystem())
Thread::Sleep(1);
The FileSystem interface exposes StartOpenFileSystem() / TryFinalizeOpenFileSystem() and StartCloseFileSystem() / TryFinalizeCloseFileSystem() for async lifecycle management. There is no per-file async open API on the FileSystem base class.
Built-in File Systems¶
Amplitude includes several built-in file system implementations:
| FileSystem | Description | Use Case |
|---|---|---|
DiskFileSystem | Standard OS file I/O | Desktop and mobile default |
PackageFileSystem | Reads from .ampk packages | Distributed builds |
AndroidAssetManagerFileSystem | Android AAssetManager | Android native apps |
NSFileSystem | iOS NSBundle | iOS native apps |
Best Practices¶
- Implement
IsValid()accurately: The engine relies on this to detect failed opens. - Support seeking: Codecs need to seek for streaming and loop support.
- Use async initialization:
StartOpenFileSystem()/TryFinalizeOpenFileSystem()prevents frame hitches. - Cache when possible: Network and archive file systems benefit from aggressive caching.
- Thread safety:
File::Read()may be called from the audio thread during streaming. Ensure your implementation is thread-safe or copy data duringOpenFile().
Next Steps¶
- Review the FileSystem API Reference.
- Learn how to use the PackageFileSystem for
.ampkfiles. - Explore the DiskFileSystem API Reference for the default implementation.