Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion include/pyraview_header.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ typedef PV_ALIGN_PREFIX(64) struct {

// API Function
// Returns 0 on success, negative values for errors
// layout: 0=SxC (Sample-Major), 1=CxS (Channel-Major)
// layout: Specifies the *Input* memory layout.
// 0=SxC (Sample-Major/Interleaved), 1=CxS (Channel-Major/Planar)
// Note: The *Output* file format is always Interleaved (Sample-Major).
int pyraview_process_chunk(
const void* dataArray, // Pointer to raw data
int64_t numRows, // Number of samples per channel
Expand Down
226 changes: 96 additions & 130 deletions src/c/pyraview.cpp

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions src/matlab/+pyraview/get_header.m
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
function header = get_header(filename)
%GET_HEADER Read the Pyraview binary header from a file.
%
% HEADER = pyraview.get_header(FILENAME) reads the standard 1024-byte header
Expand All @@ -23,3 +24,50 @@
% fprintf('Channels: %d, Rate: %.2f Hz\n', h.channelCount, h.sampleRate);
%
% See also PYRAVIEW.READFILE, PYRAVIEW.DATASET

if ~isfile(filename)
error('Pyraview:FileNotFound', 'File not found: %s', filename);
end

% Open file for binary reading, little-endian
fid = fopen(filename, 'rb', 'ieee-le');
if fid == -1
error('Pyraview:FileOpenError', 'Could not open file: %s', filename);
end

% Ensure file is closed on exit
c = onCleanup(@() fclose(fid));

% Read 1024 byte header block
% Structure layout (packed/aligned to 64 bytes, but members are standard sizes):
% char magic[4]; // 0
% uint32_t version; // 4
% uint32_t dataType; // 8
% uint32_t channelCount; // 12
% double sampleRate; // 16
% double nativeRate; // 24
% double startTime; // 32
% uint32_t decimationFactor; // 40
% uint8_t reserved[980]; // 44 .. 1023

% Read Magic
magicBytes = fread(fid, 4, '*char')';
if ~strcmp(magicBytes, 'PYRA')
error('Pyraview:InvalidHeader', 'Invalid magic string. Expected "PYRA", got "%s"', magicBytes);
end
header.magic = magicBytes;

header.version = fread(fid, 1, 'uint32');
header.dataType = fread(fid, 1, 'uint32');
header.channelCount = fread(fid, 1, 'uint32');
header.sampleRate = fread(fid, 1, 'double');
header.nativeRate = fread(fid, 1, 'double');
header.startTime = fread(fid, 1, 'double');
header.decimationFactor = fread(fid, 1, 'uint32');

% Verify size? Optional but good for sanity.
% We read 4 + 4 + 4 + 4 + 8 + 8 + 8 + 4 = 44 bytes.
% Remaining 980 bytes are reserved.

% No need to read reserved unless we want to validate file size.
end
65 changes: 65 additions & 0 deletions src/matlab/+pyraview/macOneTime.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function macOneTime()
%MACONETIME Coach the user to allow MEX files on macOS.
%
% pyraview.macOneTime()
%
% This function guides the user through the process of allowing the
% 'pyraview.mex' binary to run on macOS, which often blocks it by
% default as "unidentified developer" software.
%
% This process only needs to be done once per installation/upgrade.

if ~ismac
disp('This function is intended for macOS users only.');
return;
end

fprintf('----------------------------------------------------------------\n');
fprintf('Pyraview macOS Security Check\n');
fprintf('----------------------------------------------------------------\n');
fprintf('macOS often blocks MEX files from running because they are not\n');
fprintf('signed by an identified developer. You will likely see a popup\n');
fprintf('saying "pyraview.mex" cannot be opened.\n\n');

fprintf('INSTRUCTIONS:\n');
fprintf('1. Open "System Settings" -> "Privacy & Security".\n');
fprintf('2. Scroll down to the "Security" section.\n');
fprintf('3. Look for a message saying "pyraview.mex" was blocked.\n');
fprintf('4. Click "Allow Anyway" or "Open Anyway".\n');
fprintf(' (You might need to click "Cancel" on the popup first).\n');
fprintf('----------------------------------------------------------------\n');

input('Press Enter to attempt running pyraview.pyraview...', 's');

try
% Call with no arguments to trigger "Usage" error from C code.
% If the library is blocked, this call will throw a system error (e.g. Invalid MEX-file)
% before executing the C code.
pyraview.pyraview();
catch ME
% If it is the "InvalidInput" error from our C code, it loaded fine!
if strcmp(ME.identifier, 'Pyraview:InvalidInput')
fprintf('Success! pyraview.mex loaded successfully.\n');
else
fprintf('\nError: %s\n', ME.message);
fprintf('It seems pyraview.mex was blocked or failed to load.\n');
fprintf('Please go to System Settings -> Privacy & Security and allow it.\n');
input('Press Enter after you have allowed the file...', 's');

% Retry
try
pyraview.pyraview();
catch ME2
if strcmp(ME2.identifier, 'Pyraview:InvalidInput')
fprintf('Success! pyraview.mex loaded successfully on retry.\n');
else
fprintf('Still failed: %s\n', ME2.message);
fprintf('You may need to try again manually.\n');
end
end
end
end

fprintf('\n----------------------------------------------------------------\n');
fprintf('Security check complete. You should not need to do this again.\n');
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
#include <string.h>

/*
* pyraview_mex.c
* pyraview_gateway.c
* Gateway for Pyraview C Engine
*
* Usage:
* status = pyraview_mex(data, prefix, steps, nativeRate, startTime, [append], [numThreads])
* status = pyraview(data, prefix, steps, nativeRate, startTime, [append], [numThreads])
*
* Inputs:
* data: (Samples x Channels) matrix. uint8, int16, single, or double.
Expand All @@ -25,7 +25,7 @@
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) {
// Check inputs
if (nrhs < 5) {
mexErrMsgIdAndTxt("Pyraview:InvalidInput", "Usage: pyraview_mex(data, prefix, steps, nativeRate, startTime, [append], [numThreads])");
mexErrMsgIdAndTxt("Pyraview:InvalidInput", "Usage: pyraview(data, prefix, steps, nativeRate, startTime, [append], [numThreads])");
}

// 1. Data
Expand Down Expand Up @@ -54,7 +54,12 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) {

// Layout: Matlab is Column-Major. If input is Samples x Channels, then it is CxS.
// Layout code for CxS is 1.
int layout = 1;
// Note: The Pyraview engine uses '1' for Column-Major (Planar) layout.
// This matches MATLAB's internal storage for a Samples x Channels matrix,
// where all samples for Channel 1 are stored contiguously, followed by Channel 2, etc.
// Note: This defines the INPUT memory layout. The output file format is Sample-Major (Interleaved).
const int PV_LAYOUT_COLUMN_MAJOR = 1;
int layout = PV_LAYOUT_COLUMN_MAJOR;

// 2. Prefix
if (!mxIsChar(prhs[1])) {
Expand Down
57 changes: 0 additions & 57 deletions src/matlab/+pyraview/pyraview_get_header_mex.c

This file was deleted.

88 changes: 50 additions & 38 deletions src/matlab/+pyraview/readFile.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,10 @@
% D(:, :, 1) contains the minimum values for each sample.
% D(:, :, 2) contains the maximum values for each sample.
%
% Example:
% % Read samples 100 to 200 from 'data_L1.bin'
% d = pyraview.readFile('data_L1.bin', 100, 200);
%
% % Read from the beginning to sample 500
% d = pyraview.readFile('data_L1.bin', -Inf, 500);
%
% % Read from sample 1000 to the end
% d = pyraview.readFile('data_L1.bin', 1000, Inf);
%
% Notes:
% - Indices are clamped to the file's valid sample range.
% - Returns an empty array if the requested range is invalid or empty.
% - Pyraview files use a planar layout where channels are stored contiguously.
% - Each "sample" in a level file consists of a Min/Max pair.
% - The file format is Interleaved (Sample-Major):
% [Sample0_Ch0, Sample0_Ch1...][Sample1_Ch0, Sample1_Ch1...]
% - Each "sample point" consists of a Min/Max pair.

if ~isfile(filename)
error('Pyraview:FileNotFound', 'File not found: %s', filename);
Expand Down Expand Up @@ -68,6 +57,7 @@
dataArea = fileSize - headerSize;

numChannels = double(h.channelCount);
% Frame: Min/Max pair for ALL channels
frameSize = numChannels * 2 * itemSize;
totalSamples = floor(dataArea / frameSize);

Expand Down Expand Up @@ -108,32 +98,54 @@
if f == -1
error('Pyraview:FileOpenError', 'Could not open file: %s', filename);
end
c = onCleanup(@() fclose(f));

% Interleaved Format:
% [Header]
% [Sample 0 (Ch0..ChN)]
% [Sample 1 (Ch0..ChN)]

% Seek to Start Sample
seekOffset = headerSize + startSample * frameSize;
fseek(f, seekOffset, 'bof');

% Read block
% We read numSamplesToRead * frameSize bytes
% Raw read into vector
numElements = numSamplesToRead * numChannels * 2;
raw = fread(f, numElements, ['*' char(precision)]);

if length(raw) < numElements
warning('Pyraview:ShortRead', 'Short read. Expected %d elements, got %d.', numElements, length(raw));
% Truncate
numSamplesToRead = floor(length(raw) / (numChannels * 2));
raw = raw(1 : numSamplesToRead * numChannels * 2);
d = d(1:numSamplesToRead, :, :, :);
end

cleanupObj = onCleanup(@() fclose(f));
% Reshape
% Raw is [S0C0m S0C0M S0C1m S0C1M ... S1C0m ...]
% Shape into (2, Channels, Samples) first?
% MATLAB is Column Major.
% Reshape to (2*Channels, Samples) -> Columns are Samples.
% Data in file (Interleaved) is Sample 0 (all values), Sample 1 (all values).
% So file is stored Row-Major if mapped to (Samples x [2*Ch]).
% fread reads linearly.
% raw = [S0_Vals, S1_Vals ...]
% If we reshape to [2*Channels, NumSamples], MATLAB fills column 1 with raw(1..2*Ch).
% raw(1..2*Ch) IS S0_Vals.
% So reshape(raw, 2*numChannels, numSamplesToRead) puts Sample 0 in Column 1.
% Transpose to (NumSamples, 2*numChannels).

reshaped = reshape(raw, 2*numChannels, numSamplesToRead)';

% Now reshaped is (Samples x [2*Ch]).
% Columns: C0m C0M C1m C1M ...
% We want d(Sample, Ch, 1) = C_Ch_m
% We want d(Sample, Ch, 2) = C_Ch_M

for ch = 1:numChannels
% Calculate offset
% Planar layout: [Header] [Ch1 Data] [Ch2 Data] ...
% Ch Data size = totalSamples * 2 * itemSize

chStartOffset = headerSize + (ch-1) * totalSamples * 2 * itemSize;
readOffset = chStartOffset + startSample * 2 * itemSize;

fseek(f, readOffset, 'bof');

% Read min/max pairs
% precision needs to be char for fread
raw = fread(f, numSamplesToRead * 2, ['*' char(precision)]);

if length(raw) < numSamplesToRead * 2
warning('Pyraview:ShortRead', 'Short read on channel %d. Expected %d, got %d.', ch, numSamplesToRead*2, length(raw));
% Fill what we got
nRead = floor(length(raw)/2);
d(1:nRead, ch, 1) = raw(1:2:2*nRead);
d(1:nRead, ch, 2) = raw(2:2:2*nRead);
else
d(:, ch, 1) = raw(1:2:end);
d(:, ch, 2) = raw(2:2:end);
end
d(:, ch, 1) = reshaped(:, 2*ch - 1);
d(:, ch, 2) = reshaped(:, 2*ch);
end
end
4 changes: 2 additions & 2 deletions src/matlab/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Pyraview Matlab MEX

## Compilation
To compile the MEX file:
To compile the MEX file (`pyraview.mex`):
1. Open Matlab and `cd` to this directory.
2. Run `build_pyraview`.

## Usage
`status = pyraview_mex(data, prefix, steps, nativeRate, [append], [numThreads])`
`status = pyraview.pyraview(data, prefix, steps, nativeRate, [append], [numThreads])`

* `data`: Samples x Channels matrix (Single, Double, Int16, Uint8).
* `prefix`: Base file name (e.g. 'data/mydata').
Expand Down
7 changes: 1 addition & 6 deletions src/matlab/build_pyraview.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
include_path = '-I../../include';

% Source files inside +pyraview
mex_src = '+pyraview/pyraview_mex.c';
header_src = '+pyraview/pyraview_get_header_mex.c';
mex_src = '+pyraview/pyraview_gateway.c';

% Output directory: +pyraview/
out_dir = '+pyraview';
Expand All @@ -21,10 +20,6 @@
fprintf('Building pyraview...\n');
mex('-v', '-outdir', out_dir, '-output', 'pyraview', include_path, src_path, mex_src);
fprintf('Build pyraview successful.\n');

fprintf('Building get_header...\n');
mex('-v', '-outdir', out_dir, '-output', 'get_header', include_path, src_path, header_src);
fprintf('Build get_header successful.\n');
catch e
fprintf('Build failed: %s\n', e.message);
rethrow(e);
Expand Down
Loading