Adding itaZOOMSession class that makes audio data read-out from ZoomH6 (and...

Adding itaZOOMSession class that makes audio data read-out from ZoomH6 (and potentially other Zoom devices) more easy, especially when recording files are split into various smaller files
parent 7a576fef
function [ ref_factor, ref_db ] = calibrate( obj, cal_time, channels, method )
% Default track_id and channel_num is 1, time can be relative
% seconds of record or absolute date
% methods: 'rms', 'maximum' or 'wideband' (default)
if nargin < 4
method = 'wideband';
end
if nargin < 3
channels = 1;
end
% Extract non-calibrated (!) signal
cal_signal = obj.extract( cal_time, channels, false );
if strcmp( method, 'maximum' )
% Variates the sample truncation and uses (jst)
max_samples = 60;
for l = 1:max_samples
calibration_signal_chunk = ita_time_crop( cal_signal, [1 cal_signal.nSamples-l], 'samples' );
power(l) = max( abs( calibration_signal_chunk.freqData ) );
end
ref_db = 20*log10( max( power ) ); % == 94 dB (Sound Calibrator Type 4231)
ref_factor = max( power );
elseif strcmp( method, 'wideband' )
% Include neighbouring frequency bins around target frequency
% of 1kHz (mgu)
neighborBins = 20;
nSamplesMaxCut = 100;
justMaxVec = zeros(nSamplesMaxCut,1);
maxAndNeighborVec = zeros(nSamplesMaxCut,1);
for iSamplesCut = 1:nSamplesMaxCut
tmpSine = cal_signal; % copy original
tmpSine.timeData = tmpSine.timeData(1:end-iSamplesCut,1); % truncate signal
[justMaxVec(iSamplesCut), idxMax] = max(abs(tmpSine.freqData));
maxAndNeighborVec(iSamplesCut) = sqrt( sum(abs(tmpSine.freqData(idxMax-neighborBins:idxMax+neighborBins)).^2) );
end
ref_db = 20*log10( max( maxAndNeighborVec ) );
ref_factor = max( maxAndNeighborVec );
else
% Use root-mean-square (ITA Toolbox RMS)
rms = cal_signal.rms;
if rms > 0
ref_db = 20*log10( rms );
else
ref_db = -Inf;
end
ref_factor = rms;
end
track_ids = obj.get_track_ids( channels );
for t = 1:numel( track_ids )
obj.tracks{ track_ids( t ) }.cal_ref_factor = ref_factor;
end
end
function disp( obj )
% Displays itaZOOMSession
% <ITA-Toolbox>
% This file is part of the ITA-Toolbox. All rights reserved.
% You can find the license for this m-file in the application folder.
% </ITA-Toolbox>
disp_str = '';
disp_str = [ disp_str sprintf( ' -- itaZOOMSession -----------\n' ) ];
disp_str = [ disp_str sprintf( '\tProject name: %s\n', obj.project_name ) ];
disp_str = [ disp_str sprintf( '\tPath: %s\n', obj.path ) ];
disp_str = [ disp_str sprintf( '\tStartdate: %s\n', datestr( obj.startdate ) ) ];
disp( disp_str )
end
function snippet = extract( obj, extract_seconds, channels, calibrated )
% Default channel_num is 1, time can be relative
% seconds of record or absolute date
if numel( extract_seconds ) ~= 2
error( 'Time for extraction has to be a 1-by-2 vector with start and end value' )
end
if ( sum( extract_seconds < 0 ) || sum( extract_seconds > obj.trackLength ) )
error( 'Requested time [ %1.1fs %1.1fs ] is not (or only partly) contained in this session with track length of %1.1fs', extract_seconds( 1 ), extract_seconds( 2 ), obj.trackLength )
end
if nargin < 4
calibrated = true;
end
if nargin < 3
channels = 1;
end
if ~obj.has_channels( channels )
error( 'Could not find tracks for requested channels %i', channels )
end
snippet = itaAudio();
for c = 1:numel( channels )
track_ids = obj.get_track_ids( channels( c ) );
ets = extract_seconds; % sliding
bFirstPart = false;
bSecondPart = false;
for i=1:numel( obj.tracks{ track_ids }.part_ids )
if obj.tracks{ track_ids }.part_ids( i ) > 0
track_part_str = sprintf( '%04d', obj.tracks{ track_ids }.part_ids( i ) );
track_base_name = [ obj.identifier '_Tr' num2str( channels ) '-' track_part_str ];
else
track_base_name = [ obj.identifier '_Tr' num2str( channels ) ];
end
track_full_path = fullfile( obj.path, [ track_base_name '.wav' ] );
track_part_meta = ita_read_wav( track_full_path, 'metadata' );
part_track_length = obj.tracks{ track_ids }.trackLength_parts( i );
if ets > track_part_meta.trackLength
ets = ets - part_track_length;
continue;
end
if ets <= track_part_meta.trackLength
if ets >= 0
% requested snippet is completely in this part.
snippet = ita_read( track_full_path, ets, 'time' );
snippet.channelUnits = obj.tracks{ track_ids }.channelUnits;
if obj.tracks{ track_ids }.cal_ref_factor ~= 1 && calibrated
snippet = ita_amplify( snippet, 1 / obj.tracks{ track_ids }.cal_ref_factor );
end
return;
end
end
% Snippet overlaps track parts
if ets( 1 ) < track_part_meta.trackLength && ets( 2 ) > track_part_meta.trackLength
% beginning if snippet is in this part.
snippet_first = ita_read( track_full_path, [ ets( 1 ) track_part_meta.trackLength ], 'time' );
bFirstPart = true;
if obj.tracks{ track_ids }.cal_ref_factor ~= 1 && calibrated
snippet_first = ita_amplify( snippet_first, 1 / obj.tracks{ track_ids }.cal_ref_factor );
end
ets = ets - part_track_length;
elseif ets( 1 ) < 0 && ets( 2 ) > 0
% end of snippet is in this part.
snippet_second = ita_read( track_full_path, [ 0 ets( 2 ) ], 'time' );
bSecondPart = true;
if obj.tracks{ track_ids }.cal_ref_factor ~= 1 && calibrated
snippet_second = ita_amplify( snippet_second, 1 / obj.tracks{ track_ids }.cal_ref_factor );
end
end
end
if bFirstPart && bSecondPart
snippet = ita_append( snippet_first, snippet_second ); % Concat time data, already calibrated
snippet.channelUnits = 'Pa';
return
end
end
error( 'Requested time snippet is not found in this session, unkown error' )
end
function [ ref_factor, ref_db ] = get_calibration_factors( obj, channels )
% Returns the calibration factor (number time data has to be
% devided) and level to be subtrated for given channels
% (or all if channels option missing)
if nargin < 2
channels = 1:obj.channels;
end
ref_factor = zeros( numel( channels ), 1 );
for c=1:channels
assert( numel( c ) == 1 );
track_id = obj.get_track_ids( c );
ref_factor( c ) = obj.tracks{ track_id }.cal_ref_factor;
end
ref_db = 20 * log10( ref_factor );
end
function [ track_ids ] = get_track_ids( obj, channels )
% Returns the tracks that correspond to a certain audio channel
% on the ZOOM device (track ids are not necessarily in the same order
% as channels and there might be less tracks than the used
% channel number (i.e. if only channel 3 has been recorded)
track_ids = [];
for t = 1:numel( obj.tracks )
if obj.tracks{ t }.channel_idx == channels
track_ids( end + 1 ) = t;
end
end
end
function [ ret ] = has_channels( obj, channels )
% Checks if given channels are available
ret = zeros( numel( channels ), 1 );
for t = 1:numel( obj.tracks )
for c = 1:numel( channels )
if obj.tracks{ t }.channel_idx == channels( c )
ret( c ) = true;
end
end
end
end
\ No newline at end of file
classdef itaZOOMSession < handle
%ITAZOOMSESSION Lightweight class around a ZOOM session (a 'ZOOM0001' folder with
%recorded tracks) that supports easier calibration and extraction of
%time data
% <ITA-Toolbox>
% This file is part of the ITA-Toolbox. All rights reserved.
% You can find the license for this m-file in the application folder.
% </ITA-Toolbox>
properties(Hidden = true, Access = private)
session_valid = false;
session_ready = false;
session_calibrated = false;
tracks = cell( 0 );
end
properties( Hidden = false, Access = public )
path = '';
subfolder = '';
project_name = 'Unnamed ZOOM session';
identifier = '';
index = 0;
startdate;
channels = 0;
trackLength = 0;
samplingRate = 0;
domain = 'time';
end
methods
function obj = itaZOOMSession( session_path )
%%itaZOOMSession Create an empty ZOOM session object. If a path
% is given as first argument, this session will be loaded, too.
if nargin == 1
obj.load( session_path );
end
end
end % methods
end % class
function load( obj, session_path )
%%LOAD load a ZOOM session (folder similar to ZOOM0001)
% This method only reads metadata like start date and the file structure.
% To get content (recorded audio data) please use extract()
obj.session_valid = false;
obj.session_ready = false;
obj.session_calibrated = false;
obj.path = session_path;
[ session_subfolder, obj.identifier, ~ ] = fileparts( obj.path );
try
[ session_folder, obj.subfolder, ~ ] = fileparts( session_subfolder );
[ ~, obj.project_name, ~ ] = fileparts( session_folder );
catch
end
if strcmpi( 'ZOOM', obj.identifier( 1:4 ) )
id_cells = textscan( obj.identifier( 5:8 ), '%d' );
obj.index = id_cells{ 1 };
end
obj.session_ready = true;
lst = dir( obj.path );
for i = 1:numel( lst )
% Skip folders
if lst( i ).isdir
continue
end
[ ~, base_name, ext ] = fileparts( lst( i ).name );
% Read start date (if coded in file name)
if numel( base_name ) == 15
if strcmpi( '.hprj', ext )
dstr = lst( i ).name( 1:end-5 );
obj.startdate = datenum( dstr, 'yymmdd-HHMMSS' );
end
end
% Read track files (raw)
if strcmpi( '.wav', ext )
if strcmpi( obj.identifier, lst( i ).name( 1:numel( obj.identifier ) ) )
% Channel number
track_channel = regexp( lst( i ).name, '_Tr(\d+)', 'tokens' );
if numel( track_channel ) ~= 1
error( 'Channel number could not be extracted from track. Reading session metadata aborted.' );
end
channel_idx = str2double( track_channel{ 1 } );
track_id = 0;
if ~obj.has_channels( channel_idx )
track_id = numel( obj.tracks ) + 1;
obj.tracks{ track_id } = struct(); % append
obj.tracks{ track_id }.channel_idx = channel_idx;
obj.tracks{ track_id }.part_ids = [];
obj.tracks{ track_id }.path = fullfile( obj.path, lst( i ).name );
meta = ita_read( obj.tracks{ track_id }.path, 'metadata' );
obj.trackLength = meta.trackLength;
obj.tracks{ track_id }.trackLength_parts( 1 ) = meta.trackLength;
obj.samplingRate = meta.samplingRate;
obj.channels = obj.channels + 1;
else
track_id = obj.get_track_ids( channel_idx );
part_path = fullfile( obj.path, lst( i ).name );
meta = ita_read( part_path, 'metadata' );
obj.trackLength = obj.trackLength + meta.trackLength;
obj.tracks{ track_id }.trackLength_parts( end + 1 ) = meta.trackLength;
obj.samplingRate = meta.samplingRate;
end
obj.tracks{ track_id }.cal_ref_factor = 1.0;
obj.tracks{ track_id }.channelUnits = 'Pa';
% Track part running index of splitted files (may be empty or vector)
track_part_id_raw = regexp( lst( i ).name, '-(\d+).WAV', 'tokens' );
part_id = 0;
if numel( track_part_id_raw ) > 0
part_id = str2double( track_part_id_raw{ : } );
end
obj.tracks{ track_id }.part_ids( end + 1 ) = part_id;
end
end
end
obj.session_valid = true;
end
\ No newline at end of file
function relative_times = relativetime( obj, absolute_dates )
% Returns the relative time in seconds from beginning of session to
% given absolute date(s). Handy for extracting data with absolute time
% values, i.e. in combination with datenum( '...' )
relative_times = ( absolute_dates - obj.startdate ) * 24 * 60 * 60; % seconds
if relative_times > obj.trackLength
error( 'Absolute date(s) exceed session end time' )
end
if relative_times < 0
error( 'Absolute date(s) are earlier than session start time' )
end
end
function set_calibration_factors( obj, cal_ref_factor, channels )
% Sets the calibration factor (number time data has to be
% devided) for given channels (or all if channels option
% missing)
if nargin < 3
channels = obj.channels;
end
if cal_ref_factor <= 0
error( 'Calibration factor has to be greater zero, was %f', cal_ref_factor )
end
track_ids = obj.get_track_ids( channels );
for i=1:track_ids
obj.tracks{ i }.cal_ref_factor = cal_ref_factor;
end
end
zoom_session_data_path = 'D:\Users\stienen\Sciebo\ITA\Projekte\Virtual Reality\ITA_FrontYard\recordings\Zoom_H6\ZOOM0002';
zobj = itaZOOMSession( zoom_session_data_path )
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment