EDF_join

EDF_join(input_spec, varargin)

EDF_JOIN Join split EDF / EDF+ segments into one continuous recording.

out_path = EDF_join(filelist) out_path = EDF_join(filelist, ‘OutputName’, name, …) [out_path, header, signal_header, signal_cell, annotations] = EDF_join(…)

Reads a set of EDF files that are pieces of a single recording (a clinical acquisition split into multiple files, with or without gaps between them), verifies that they belong together, orders them by recording start date/time, and writes ONE continuous EDF spanning the full time range. Any gap between the end of one segment and the start of the next is filled with a constant “blank” value (0 in physical units by default) so that every sample from the earliest start to the latest end is present and correctly time-aligned.

Inputs:
input_specone of
  • cell array of file paths (.edf/.edf.gz/.edf.zst)

  • directory string (globbed with ‘Pattern’)

  • text file path with one filename per line

Name-value pairs:
‘OutputName’explicit output path. Default:

<dir>/<first_basename>_joined<ext>, where <ext> follows ‘CompressMode’.

‘CompressMode’‘zstd’ (default) | ‘gzip’ | ‘none’. Sets the

default output extension (.edf.zst / .edf.gz / .edf) when ‘OutputName’ is not given.

‘GzipLevel’ : integer 1..9 (default 6). Used for .gz output. ‘ZstdLevel’ : integer 1..22 (default 9). Used for .zst output. ‘AutoScale’ : ‘preserve’ (default) | ‘recompute’. Passed to

write_EDF. ‘preserve’ keeps each channel’s stored physical_min/max (lossless when all segments share the same scaling); ‘recompute’ resets the range from the joined data. If the segments do NOT share identical per-channel physical/digital ranges, or the fill value falls outside a channel’s stored range where a gap exists, ‘recompute’ is the safe choice (a warning points this out).

‘FillValue’constant physical-unit value written into gaps

(default 0). Applies to every non-annotation channel.

‘StartTimes’override the per-file start datetime instead of

parsing it from the headers. Cell array or datetime array with one entry per input file, in the SAME order as the resolved file list (see ‘Verbose’ to print that order). Entries may be datetime, datenum, or a parseable date string. Use this when header dates are missing or wrong.

‘Tolerance’alignment/overlap tolerance in seconds

(default 1e-3). A segment must start an integer number of records after the global start to sit on the continuous record grid; deviations larger than this are an error. Segment time ranges that overlap by more than this are also an error.

‘Pattern’*.edf’ (default) glob when input_spec is a

directory (also matches .edf.gz / .edf.zst).

‘Verbose’logical (default false). Prints the resolved,

sorted segment list with start times, gaps, and the joined total duration.

‘forceMATLAB’logical (default false). Forwarded to read_EDF /

write_EDF to bypass the MEX backends.

Outputs:

out_path : full path of the written joined file header : joined file-level header struct (as written) signal_header : joined per-signal header struct array signal_cell : joined per-channel physical-unit vectors annotations : merged EDF+ annotations (onsets shifted to the

joined timeline)

datetime is taken from the EDF+ ‘Startdate DD-MMM-YYYY’ token in local_rec_id when present (4-digit year, unambiguous), otherwise from the main-header recording_startdate ‘dd.mm.yy’ (EDF 1985 century rule) plus recording_starttime. A missing/invalid start time is an error unless supplied via ‘StartTimes’.

  • Record griddata_record_duration must be identical across

    segments, and each segment must begin an integer number of records after the earliest start (so samples land on one shared record grid). A non-integer offset larger than ‘Tolerance’ is an error — the segments cannot be tiled exactly.

  • No overlapsegment [start, end) time ranges must not

    overlap (touching end-to-start is fine, that is a seamless join). Any overlap beyond ‘Tolerance’ is an error, since overlapping samples would be ambiguous.

  • Identical : the ordered list of non-annotation channel montage labels must match across all segments, and each

    channel’s samples_in_record (hence sampling rate) must match. Differences are an error. Differing physical/digital ranges are a WARNING (use ‘recompute’ to avoid re-quantization loss).

Gaps between segments are filled with ‘FillValue’ (physical units) on every non-annotation channel. EDF+ annotation channels are regenerated by write_EDF for the full continuous span; annotations from every segment are merged with their onsets shifted onto the joined timeline.

% Join three explicitly-listed segments (zstd output next to file 1) EDF_join({‘night_part1.edf’, ‘night_part2.edf’, ‘night_part3.edf’});

% Join every EDF in a directory, plain .edf output at a set path EDF_join(‘/data/split_night’, …

‘CompressMode’, ‘none’, ‘OutputName’, ‘/data/night_full.edf’, … ‘Verbose’, true);

% Headers have unreliable dates — supply start times explicitly EDF_join({‘a.edf’,’b.edf’}, …

‘StartTimes’, {datetime(2024,3,1,22,0,0), datetime(2024,3,1,23,5,0)});

% Also capture the joined structures without re-reading [out, hdr, shdr, sc, ann] = EDF_join(files);

Memory note: every segment is read fully into memory and the joined recording is assembled in memory before writing, so peak use is roughly the input total plus the output. For very large cohorts, join in groups.

See also: read_EDF, write_EDF, convert_EDF, batch_convert_EDF.