API Documentation
Below is the documentation for all functions exported by Onda.jl. For general information regarding the Onda format, please see beacon-biosignals/OndaFormat.
Note that Onda.jl's API follows a specific philosophy with respect to property access: users are generally expected to access fields via Julia's object.fieldname syntax, but should only mutate objects via the exposed API methods documented below.
Dataset API
Onda.Dataset — TypeDataset(path)Return a Dataset instance targeting path as an Onda dataset, without loading any content from path.
Onda.load — Functionload(path)Return a Dataset instance that contains all metadata necessary to read and write to the Onda dataset stored at path. Note that this constuctor loads all the Recording objects contained in path/recordings.msgpack.zst.
load(dataset::Dataset, uuid::UUID, signal_name::Symbol[, span::AbstractTimeSpan])Load, decode, and return the Samples object corresponding to the signal named signal_name in the recording specified by uuid.
If span is provided, this function returns the equivalent of load(dataset, uuid, signal_name)[:, span], but potentially avoids loading the entire signal's worth of sample data if the underlying signal file format and target storage layer both support partial access/random seeks.
load(dataset::Dataset, uuid::UUID[, span::AbstractTimeSpan])Return load(dataset, uuid, names[, span]) where names is a list of all signal names in the recording specified by uuid.
load(dataset::Dataset, uuid::UUID, signal_names[, span::AbstractTimeSpan])Return Dict(signal_name => load(dataset, uuid, signal_name[, span]) for signal_name in signal_names).
See also: read_samples, deserialize_lpcm
Onda.load_encoded — Functionload_encoded(args...)Supports exactly the same methods as load, but doesn't automatically call decode on the returned Samples.
Onda.save — Functionsave(dataset::Dataset)Save all metadata content necessary to read/write dataset to dataset.path.
Note that in-memory mutations to dataset will not persist unless followed by a save call. Furthermore, new sample data written to dataset via store! will not be readable from freshly loaded copies of dataset (e.g. load(dataset.path)) until save is called.
Onda.create_recording! — Functioncreate_recording!(dataset::Dataset, uuid::UUID=uuid4())Create uuid::UUID => recording::Recording, add the pair to dataset.recordings, and return the pair.
Onda.store! — Functionstore!(dataset::Dataset, uuid::UUID, signal_name::Symbol, samples::Samples;
       overwrite::Bool=true)Add signal_name => samples.signal to dataset.recordings[uuid].signals and serialize samples.data to the proper file path within dataset.path.
If overwrite is false, an error is thrown if a signal with signal_name already exists in dataset.recordings[uuid]. Otherwise, existing entries matching samples.signal will be deleted and replaced with samples.
Base.delete! — Functiondelete!(dataset::Dataset, uuid::UUID)Delete the recording whose UUID matches uuid from dataset. This function removes the matching Recording object from dataset.recordings, as well as deletes the corresponding subdirectory in the dataset's samples directory.
delete!(dataset::Dataset, uuid::UUID, signal_name::Symbol)Delete the signal whose signalname matches `signalnamefrom the recording whose UUID matchesuuidindataset. This function removes the matchingSignalobject fromdataset.recordings[uuid], as well as deletes the corresponding sample data in thedataset'ssamples` directory.
Onda.validate_on_construction — FunctionOnda.validate_on_construction()If this function returns true, Onda objects will be validated upon construction for compliance with the Onda specification.
If this function returns false, no such validation will be performed upon construction.
Users may interactively redefine this method in order to attempt to read malformed Onda datasets.
Returns true by default.
See also: validate_signal, validate_samples
Onda Format Metadata
Onda.Signal — TypeSignalA type representing an individual Onda signal object. Instances contain the following fields, following the Onda specification for signal objects:
- channel_names::Vector{Symbol}
- start_nanosecond::Nanosecond
- stop_nanosecond::Nanosecond
- sample_unit::Symbol
- sample_resolution_in_unit::Float64
- sample_offset_in_unit::Float64
- sample_type::DataType
- sample_rate::Float64
- file_extension::Symbol
- file_options::Union{Nothing,Dict{Symbol,Any}}
If validate_on_construction returns true, validate_signal is called on all new Signal instances upon construction.
Similarly to the TimeSpan constructor, this constructor will add a single Nanosecond to stop_nanosecond if start_nanosecond == stop_nanosecond.
Onda.validate_signal — Functionvalidate_signal(signal::Signal)Returns nothing, checking that the given signal is valid w.r.t. the Onda specification. If a violation is found, an ArgumentError is thrown.
Properties that are validated by this function include:
- sample_typeis a valid Onda sample type
- sample_unitname is lowercase, snakecase, and alphanumeric
- start_nanosecond/- stop_nanosecondform a valid time span
- channel names are lowercase, snakecase, and alphanumeric
Onda.signal_from_template — Functionsignal_from_template(signal::Signal;
                     channel_names=signal.channel_names,
                     start_nanosecond=signal.start_nanosecond,
                     stop_nanosecond=signal.stop_nanosecond,
                     sample_unit=signal.sample_unit,
                     sample_resolution_in_unit=signal.sample_resolution_in_unit,
                     sample_offset_in_unit=signal.sample_offset_in_unit,
                     sample_type=signal.sample_type,
                     sample_rate=signal.sample_rate,
                     file_extension=signal.file_extension,
                     file_options=signal.file_options,
                     validate=Onda.validate_on_construction())Return a Signal where each field is mapped to the corresponding keyword argument.
Onda.span — Functionspan(signal::Signal)Return TimeSpan(signal.start_nanosecond, signal.stop_nanosecond).
Onda.sizeof_samples — Functionsizeof_samples(signal::Signal)Returns the expected size (in bytes) of the encoded Samples object corresponding to the entirety of signal:
sample_count(signal) * channel_count(signal) * sizeof(signal.sample_type)Onda.Annotation — TypeAnnotation <: AbstractTimeSpanA type representing an individual Onda annotation object. Instances contain the following fields, following the Onda specification for annotation objects:
- value::String
- start_nanosecond::Nanosecond
- stop_nanosecond::Nanosecond
Similarly to the TimeSpan constructor, this constructor will add a single Nanosecond to stop_nanosecond if start_nanosecond == stop_nanosecond.
Onda.Recording — TypeRecordingA type representing an individual Onda recording object. Instances contain the following fields, following the Onda specification for recording objects:
- signals::Dict{Symbol,Signal}
- annotations::Set{Annotation}
Onda.set_span! — Functionset_span!(recording::Recording, name::Symbol, span::AbstractTimeSpan)Replace recording.signals[name] with a copy that has the start_nanosecond and start_nanosecond fields set to match the provided span. Returns the newly constructed Signal instance.
set_span!(recording::Recording, span::TimeSpan)Return Dict(name => set_span!(recording, name, span) for name in keys(recording.signals))
Onda.annotate! — Functionannotate!(recording::Recording, annotation::Annotation)Returns push!(recording.annotations, annotation).
Samples
Onda.Samples — TypeSamples(signal::Signal, encoded::Bool, data::AbstractMatrix,
        validate::Bool=Onda.validate_on_construction())Return a Samples instance with the following fields:
- signal::Signal: The- Signalobject that describes the- Samplesinstance.
- encoded::Bool: If- true, the values in- dataare LPCM-encoded as prescribed by the- Samplesinstance's- signal. If- false, the values in- datahave been decoded into the- signal's canonical units.
- data::AbstractMatrix: A matrix of sample data. The- ith row of the matrix corresponds to the- ith channel in- signal.channel_names, while the- jth column corresponds to the- jth multichannel sample.
- validate::Bool: If- true,- validate_samplesis called on the constructed- Samplesinstance before it is returned.
Note that getindex and view are defined on Samples to accept normal integer indices, but also accept channel names for row indices and TimeSpan values for column indices; see Onda/examples/tour.jl for a comprehensive set of indexing examples.
Base.:== — Method==(a::Samples, b::Samples)Returns a.encoded == b.encoded && a.signal == b.signal && a.data == b.data.
Onda.validate_samples — Functionvalidate_samples(samples::Samples)Returns nothing, checking that the given samples are valid w.r.t. the underlying samples.signal and the Onda specification's canonical LPCM representation. If a violation is found, an ArgumentError is thrown.
Properties that are validated by this function include:
- encoded element type matches samples.signal.sample_type
- the number of rows of samples.datamatches the number of channels insamples.signal
Onda.channel — Functionchannel(signal::Signal, name::Symbol)Return i where signal.channel_names[i] == name.
channel(signal::Signal, i::Integer)Return signal.channel_names[i].
channel(samples::Samples, name::Symbol)Return channel(samples.signal, name).
This function is useful for indexing rows of samples.data by channel names.
channel(samples::Samples, i::Integer)Return channel(samples.signal, i).
Onda.channel_count — Functionchannel_count(signal::Signal)Return length(signal.channel_names).
channel_count(samples::Samples)Return channel_count(samples.signal).
Onda.sample_count — Functionsample_count(signal::Signal)Return the number of multichannel samples that fit within duration(signal) given signal.sample_rate.
sample_count(samples::Samples)Return the number of multichannel samples in samples (i.e. size(samples.data, 2))
sample_count(samples) is not generally equivalent to sample_count(samples.signal); the former is the sample count of the entire original signal in the context of its parent recording, whereas the latter is actual number of multichannel samples in samples.data.
Onda.encode — Functionencode(sample_type::DataType, sample_resolution_in_unit, sample_offset_in_unit,
       samples, dither_storage=nothing)Return a copy of samples quantized according to sample_type, sample_resolution_in_unit, and sample_offset_in_unit. sample_type must be a concrete subtype of Onda.VALID_SAMPLE_TYPE_UNION. Quantization of an individual sample s is performed via:
round(S, (s - sample_offset_in_unit) / sample_resolution_in_unit)with additional special casing to clip values exceeding the encoding's dynamic range.
If dither_storage isa Nothing, no dithering is applied before quantization.
If dither_storage isa Missing, dither storage is allocated automatically and triangular dithering is applied to the signal prior to quantization.
Otherwise, dither_storage must be a container of similar shape and type to samples. This container is then used to store the random noise needed for the triangular dithering process, which is applied to the signal prior to quantization.
encode(samples::Samples, dither_storage=nothing)If samples.encoded is false, return a Samples instance that wraps:
encode(samples.signal.sample_type,
       samples.signal.sample_resolution_in_unit,
       samples.signal.sample_offset_in_unit,
       samples.data, dither_storage)If samples.encoded is true, this function is the identity.
Onda.encode! — Functionencode!(result_storage, sample_type::DataType, sample_resolution_in_unit,
        sample_offset_in_unit, samples, dither_storage=nothing)
encode!(result_storage, sample_resolution_in_unit, sample_offset_in_unit,
        samples, dither_storage=nothing)Similar to encode(sample_type, sample_resolution_in_unit, sample_offset_in_unit, samples, dither_storage), but write encoded values to result_storage rather than allocating new storage.
sample_type defaults to eltype(result_storage) if it is not provided.
encode!(result_storage, samples::Samples, dither_storage=nothing)If samples.encoded is false, return a Samples instance that wraps:
encode!(result_storage,
        samples.signal.sample_type,
        samples.signal.sample_resolution_in_unit,
        samples.signal.sample_offset_in_unit,
        samples.data, dither_storage)`.If samples.encoded is true, return a Samples instance that wraps copyto!(result_storage, samples.data).
Onda.decode — Functiondecode(sample_resolution_in_unit, sample_offset_in_unit, samples)Return sample_resolution_in_unit .* samples .+ sample_offset_in_unit
decode(samples::Samples)If samples.encoded is true, return a Samples instance that wraps decode(samples.signal.sample_resolution_in_unit, samples.signal.sample_offset_in_unit, samples.data).
If samples.encoded is false, this function is the identity.
Onda.decode! — Functiondecode!(result_storage, sample_resolution_in_unit, sample_offset_in_unit, samples)Similar to decode(sample_resolution_in_unit, sample_offset_in_unit, samples), but write decoded values to result_storage rather than allocating new storage.
decode!(result_storage, samples::Samples)If samples.encoded is true, return a Samples instance that wraps decode!(result_storage, samples.signal.sample_resolution_in_unit, samples.signal.sample_offset_in_unit, samples.data).
If samples.encoded is false, return a Samples instance that wraps copyto!(result_storage, samples.data).
AbstractTimeSpan
Onda.AbstractTimeSpan — TypeAbstractTimeSpanA type repesenting a continuous, inclusive span between two points in time.
All subtypes of AbstractTimeSpan must implement:
- first(::AbstractTimeSpan)::Nanosecond: return the first nanosecond contained in- span
- last(::AbstractTimeSpan)::Nanosecond: return the last nanosecond contained in- span
For convenience, many Onda functions that accept AbstractTimeSpan values also accept Dates.Period values.
See also: TimeSpan
Onda.TimeSpan — TypeTimeSpan(first, last)Return TimeSpan(Nanosecond(first), Nanosecond(last))::AbstractTimeSpan.
If first == last, a single Nanosecond is added to last since last is an exclusive upper bound and Onda only supports up to nanosecond precision anyway. This behavior also avoids most practical forms of potential breakage w.r.t to legacy versions of Onda that accidentally allowed the construction of TimeSpans where first == last.
See also: AbstractTimeSpan
Onda.contains — Functioncontains(a::AbstractTimeSpan, b::AbstractTimeSpan)Return true if the timespan b lies entirely within the timespan a, return false otherwise.
Onda.overlaps — Functionoverlaps(a, b)Return true if the timespan a and the timespan b overlap, return false otherwise.
Onda.shortest_timespan_containing — Functionshortest_timespan_containing(spans)Return the shortest possible TimeSpan containing all timespans in spans.
spans is assumed to be an iterable of timespans.
Onda.duration — Functionduration(span)Return the duration of span as a Period.
For span::AbstractTimeSpan, this is equivalent to last(span) - first(span).
For span::Period, this function is the identity.
duration(signal::Signal)Return duration(span(signal)).
duration(recording::Recording)Returns maximum(s -> s.stop_nanosecond, values(recording.signals)); throws an ArgumentError if recording.signals is empty.
duration(samples::Samples)Returns the Nanosecond value for which samples[TimeSpan(0, duration(samples))] == samples.data.
duration(samples) is not generally equivalent to duration(samples.signal); the former is the duration of the entire original signal in the context of its parent recording, whereas the latter is the actual duration of samples.data given samples.signal.sample_rate.
Onda.time_from_index — Functiontime_from_index(sample_rate, sample_index)Given sample_rate in Hz and assuming sample_index > 0, return the earliest Nanosecond containing sample_index.
Examples:
julia> time_from_index(1, 1)
0 nanoseconds
julia> time_from_index(1, 2)
1000000000 nanoseconds
julia> time_from_index(100, 100)
990000000 nanoseconds
julia> time_from_index(100, 101)
1000000000 nanosecondstime_from_index(sample_rate, sample_range::AbstractUnitRange)Return the TimeSpan corresponding to sample_range given sample_rate in Hz:
julia> time_from_index(100, 1:100)
TimeSpan(0 nanoseconds, 1000000000 nanoseconds)
julia> time_from_index(100, 101:101)
TimeSpan(1000000000 nanoseconds, 1000000001 nanoseconds)
julia> time_from_index(100, 301:600)
TimeSpan(3000000000 nanoseconds, 6000000000 nanoseconds)Onda.index_from_time — Functionindex_from_time(sample_rate, sample_time)Given sample_rate in Hz, return the integer index of the most recent sample taken at sample_time. Note that sample_time must be non-negative and support convert(Nanosecond, sample_time).
Examples:
julia> index_from_time(1, Second(0))
1
julia> index_from_time(1, Second(1))
2
julia> index_from_time(100, Millisecond(999))
100
julia> index_from_time(100, Millisecond(1000))
101index_from_time(sample_rate, span::AbstractTimeSpan)Return the UnitRange of indices corresponding to span given sample_rate in Hz:
julia> index_from_time(100, TimeSpan(Second(0), Second(1)))
1:100
julia> index_from_time(100, TimeSpan(Second(1)))
101:101
julia> index_from_time(100, TimeSpan(Second(3), Second(6)))
301:600Paths API
Onda's Paths API directly underlies its Dataset API, providing an abstraction layer that can be overloaded to support new storage backends for sample data and recording metadata. This API's fallback implementation supports any path-like type P that supports:
- Base.read(::P)
- Base.write(::P, bytes::Vector{UInt8})
- Base.rm(::P; force, recursive)
- Base.joinpath(::P, ::AbstractString...)
- Base.mkpath(::P)(note: this is allowed to be a no-op for storage backends which have no notion of intermediate directories, e.g. object storage systems)
- Base.dirname(::P)
- Onda.read_byte_range(see signatures documented below)
Onda.read_recordings_file — Functionread_recordings_file(path)Return deserialize_recordings_msgpack_zst(read(path)).
Onda.write_recordings_file — Functionwrite_recordings_file(path, header::Header, recordings::Dict{UUID,Recording})Write serialize_recordings_msgpack_zst(header, recordings) to path.
Onda.samples_path — Functionsamples_path(dataset_path, uuid::UUID)Return the path to the samples subdirectory within dataset_path corresponding to the recording specified by uuid.
samples_path(dataset_path, uuid::UUID, signal_name, file_extension)Return the path to the sample data within dataset_path corresponding to the given signal information and the recording specified by uuid.
samples_path(dataset::Dataset, uuid::UUID)Return samples_path(dataset.path, uuid).
samples_path(dataset::Dataset, uuid::UUID, signal_name::Symbol)Return samples_path(dataset.path, uuid, signal_name, extension) where extension is defined as dataset.recordings[uuid].signals[signal_name].file_extension.
Onda.read_samples — Functionread_samples(path, signal::Signal)Return the Samples object described by signal and stored at path.
read_samples(path, signal::Signal, span::AbstractTimeSpan)Return read_samples(path, signal)[:, span], but attempt to avoid reading unreturned intermediate sample data. Note that the effectiveness of this method depends on the types of both path and format(signal).
Onda.write_samples — Functionwrite_samples(path, samples::Samples)Serialize and write encode(samples) to path.
Onda.read_byte_range — Functionread_byte_range(path, byte_offset, byte_count)Return the equivalent read(path)[(byte_offset + 1):(byte_offset + byte_count)], but try to avoid reading unreturned intermediate bytes. Note that the effectiveness of this method depends on the type of path.
Serialization API
Onda's Serialization API underlies its Paths API, providing a storage-agnostic abstraction layer that can be overloaded to support new file/byte formats for (de)serializing LPCM-encodeable sample data. This API also facilitates low-level streaming sample data (de)serialization and Onda metadata (de)serialization.
Onda.deserialize_recordings_msgpack_zst — Functiondeserialize_recordings_msgpack_zst(bytes::Vector{UInt8})Return the (header::Header, recordings::Dict{UUID,Recording}) yielded from deserializing bytes, which is assumed to be in zstd-compressed MsgPack format and comply with the Onda format's specification of the contents of recordings.msgpack.zst.
Onda.serialize_recordings_msgpack_zst — Functionserialize_recordings_msgpack_zst(header::Header, recordings::Dict{UUID,Recording})Return the Vector{UInt8} that results from serializing (header::Header, recordings::Dict{UUID,Recording}) to zstd-compressed MsgPack format.
Onda.AbstractLPCMFormat — TypeAbstractLPCMFormatA type whose subtypes represents byte/stream formats that can be (de)serialized to/from Onda's standard interleaved LPCM representation.
All subtypes of the form F<:AbstractLPCMFormat must support a constructor of the form F(::Signal) and overload Onda.format_constructor_for_file_extension with the appropriate file extension.
See also:
Onda.AbstractLPCMStream — TypeAbstractLPCMStreamA type that represents an LPCM (de)serialization stream.
See also:
Onda.deserializing_lpcm_stream — Functiondeserializing_lpcm_stream(format::AbstractLPCMFormat, io)Return a stream::AbstractLPCMStream that wraps io to enable direct LPCM deserialization from io via deserialize_lpcm.
Note that stream must be finalized after usage via finalize_lpcm_stream. Until stream is finalized, io should be considered to be part of the internal state of stream and should not be directly interacted with by other processes.
Onda.serializing_lpcm_stream — Functionserializing_lpcm_stream(format::AbstractLPCMFormat, io)Return a stream::AbstractLPCMStream that wraps io to enable direct LPCM serialization to io via serialize_lpcm.
Note that stream must be finalized after usage via finalize_lpcm_stream. Until stream is finalized, io should be considered to be part of the internal state of stream and should not be directly interacted with by other processes.
Onda.finalize_lpcm_stream — Functionfinalize_lpcm_stream(stream::AbstractLPCMStream)::BoolFinalize stream, returning true if the underlying I/O object used to construct stream is still open and usable. Otherwise, return false to indicate that underlying I/O object was closed as result of finalization.
Onda.format_constructor_for_file_extension — FunctionOnda.format_constructor_for_file_extension(::Val{:extension_symbol})Return a constructor of the form F(::Signal)::AbstractLPCMFormat corresponding to the provided extension.
This function should be overloaded for new AbstractLPCMFormat subtypes.
Onda.format — Functionformat(signal::Signal; kwargs...)Return F(signal; kwargs...) where F is the AbstractLPCMFormat that corresponds to signal.file_extension (as determined by the format author via format_constructor_for_file_extension).
See also: deserialize_lpcm, serialize_lpcm
Onda.deserialize_lpcm — Functiondeserialize_lpcm(format::AbstractLPCMFormat, bytes,
                 samples_offset::Integer=0,
                 samples_count::Integer=typemax(Int))
deserialize_lpcm(stream::AbstractLPCMStream,
                 samples_offset::Integer=0,
                 samples_count::Integer=typemax(Int))Return a channels-by-timesteps AbstractMatrix of interleaved LPCM-encoded sample data by deserializing the provided bytes in the given format, or from the given stream constructed by deserializing_lpcm_stream.
Note that this operation may be performed in a zero-copy manner such that the returned sample matrix directly aliases bytes.
The returned segment is at most sample_offset samples offset from the start of stream/bytes and contains at most sample_count samples. This ensures that overrun behavior is generally similar to the behavior of Base.skip(io, n) and Base.read(io, n).
This function is the inverse of the corresponding serialize_lpcm method, i.e.:
serialize_lpcm(format, deserialize_lpcm(format, bytes)) == bytesOnda.deserialize_lpcm_callback — Functiondeserialize_lpcm_callback(format::AbstractLPCMFormat, samples_offset, samples_count)Return (callback, required_byte_offset, required_byte_count) where callback accepts the byte block specified by required_byte_offset and required_byte_count and returns the samples specified by samples_offset and samples_count.
As a fallback, this function returns (callback, missing, missing), where callback requires all available bytes. AbstractLPCMFormat subtypes that support partial/block-based deserialization (e.g. the basic LPCM format) can overload this function to only request exactly the byte range that is required for the sample range requested by the caller.
This allows callers to handle the byte block retrieval themselves while keeping Onda's LPCM Serialization API agnostic to the caller's storage layer of choice.
Onda.serialize_lpcm — Functionserialize_lpcm(format::AbstractLPCMFormat, samples::AbstractMatrix)
serialize_lpcm(stream::AbstractLPCMStream, samples::AbstractMatrix)Return the AbstractVector{UInt8} of bytes that results from serializing samples to the given format (or serialize those bytes directly to stream) where samples is a channels-by-timesteps matrix of interleaved LPCM-encoded sample data.
Note that this operation may be performed in a zero-copy manner such that the returned AbstractVector{UInt8} directly aliases samples.
This function is the inverse of the corresponding deserialize_lpcm method, i.e.:
deserialize_lpcm(format, serialize_lpcm(format, samples)) == samplesOnda.LPCM — TypeLPCM{S}(channel_count)
LPCM(signal::Signal)Return a LPCM<:AbstractLPCMFormat instance corresponding to Onda's default interleaved LPCM format assumed for sample data files with the "lpcm" extension.
S corresponds to signal.sample_type, while channel_count corresponds to length(signal.channel_names).
Note that bytes (de)serialized to/from this format are little-endian (per the Onda specification).
Onda.LPCMZst — TypeLPCMZst(lpcm::LPCM; level=3)
LPCMZst(signal::Signal; level=3)Return a LPCMZst<:AbstractLPCMFormat instance that corresponds to Onda's default interleaved LPCM format compressed by zstd. This format is assumed for sample data files with the "lpcm.zst" extension.
The level keyword argument sets the same compression level parameter as the corresponding flag documented by the zstd command line utility.
See https://facebook.github.io/zstd/ for details about zstd.
Upgrading Older Datasets to Newer Datasets
Onda.upgrade_onda_format_from_v0_2_to_v0_3! — FunctionOnda.upgrade_onda_format_from_v0_2_to_v0_3!(path, combine_annotation_key_value)Upgrade the Onda v0.2 dataset at path to a Onda v0.3 dataset, returning the upgraded Dataset. This upgrade process overwrites path/recordings.msgpack.zst with a v0.3-compliant version of this file; for safety's sake, the old v0.2 file is preserved at path/old.recordings.msgpack.zst.backup.
A couple of the Onda v0.2 -> v0.3 changes require some special handling:
- The - customfield was removed from recording objects. This function thus writes out a file at- path/recordings_custom.msgpack.zstthat contains a map of UUIDs to corresponding recordings'- customvalues before deleting the- customfield. This file can be deserialized via- MsgPack.unpack(Onda.zstd_decompress(read("recordings_custom.msgpack.zst"))).
- Annotations no longer have a - keyfield. Thus, each annotation's existing- keyand- valuefields are combined into the single new- valuefield via the provided callback- combine_annotation_key_value(annotation_key, annotation_value).