Code covered by the BSD License  

Highlights from
Simple Audio Editor

image thumbnail

Simple Audio Editor

by

 

09 May 2008 (Updated )

This is a simple audio editor GUI to read, write, cut, copy, paste, filter and analyze audio data.

AudioEditor
classdef AudioEditor < handle
%AudioEditor Create audio editor GUI
%   AudioEditor creates a GUI used for editing audio data. It supports
%   reading from a WAVE audio file, writing to a WAVE audio file, importing
%   from a MATLAB variable, exporting to a MATLAB variable and recording
%   from microphone. There are editing features like cut, copy, paste with
%   undo and redo support for them. There are also filters to process the
%   audio data and analyzers to analyze the data.
%
%   The file reading and writing support can be extended to other formats
%   by writing add-ons. For example, see the files in directory
%   +filereaders and +filewriters. Similarly the filters and analyzers also
%   can be extended. Look at the directories +filters and +analyzers for
%   examples of implementing them.
%
%   Importing and Exporting data (Data menu):
%    Audio data can be read from a file, imported from MATLAB expression or
%    recorded from microphone. Audio data can be written to a file and
%    exported to MATLAB workspace. All these operations can be performed
%    using the options available under the File menu. The File menu also
%    holds a list of most recent files used. Currently the default file
%    reader/writer supports only the files that can be read/write using
%    wavread/wavwrite commands.
%
%   Selecting data:
%    Data can be selected by clicking and dragging with the mouse on the
%    plot. The selection can be extended or reduced by right clicking on
%    the plot. The selected data will be shown with a different background
%    color and with red lines at the border on both ends.
%
%   Editing data:
%    Selected data can be cut or copied to clipboard by choosing the Cut or
%    Copy option under Edit menu. Data from clipboard can be pasted using
%    the Paste option under Edit menu. If no data is selected the data is
%    pasted at the cursor line. If some data is selected, the selected data
%    will be replaced by data from the clipboard. All the Cut, Copy and
%    Paste operations can be un-done or re-done. The Undo and Redo
%    operations are possible until new data is imported into the
%    AudioEditor.
%
%   Filtering data:
%    Selected data can be filtered by using the options from the Filter
%    menu. The selected data is passed through the filter and then the
%    result is used to replace the selected data. By default three filters
%    Digital Filter, Downsample and MATLAB Expression are available.
%    Digital Filter can be used to implement any linear filtering
%    operation by specifying the numerator and denominator coefficients of
%    the transfer function. Downsample can be used to down sample data by
%    any integer value. MATLAB Expression option can be used to operate
%    over the data using any MATLAB command.
%
%   Analyzing data:
%    The data can be analyzed by using the options under Analyze menu. By
%    default Welch PSD is available to analyze the frequency contents of
%    the data. If any data is selected, then the psd is taken for the
%    selected data. If no data is selected, then the psd is taken for 2048
%    samples around the cursor. When audio data is played or when the
%    cursor is moved the analysis window is continuously updated by using
%    the 2048 samples around the cursor.
%
%   Playback Buttons:
%    The play button plays the audio from the cursor location to the end if
%    no audio data is selected. Otherwise it plays the selected data. The
%    pause button pauses audio play and play can be resumed by pressing
%    the pause button again. Pressing the stop button stops audio playback.
%    Pressing the play button again when audio is being played stops the
%    current play and the play is started again from the original starting
%    location.
%
%   Other buttons:
%    File open button performs the same operation as the Read From File
%    option under File menu. The Save button performs the same operation as
%    the Write To File option under File menu. 
%
%    The Zoom in and Zoom out buttons perform zooming operation in the
%    x-axis. The button Fit Selection to axes, zooms the selected data to
%    fill the entire x-axis. The button Fit entire signal to axes does the
%    opposite, by fitting the entire audio signal in the axis.
%
%   Extending the AudioEditor:
%    You can implement your own filters and analyzers. The AudioEditor will
%    automatically identify and populate them in the corresponding menu.
%    For details see example code in the +filters and +analyzers directory.
%
%   Down sampled plot:
%    If the audio data to be plotted is very large, then the data is
%    downsampled before plotting for faster response. While down sampling,
%    the outliers in the data are detected and added to the data making the
%    plot look similar to plotting the orignal data. This is based on
%    the code from dsplot plotting routine by Jiro Doke in MATLAB Central.
%
%   Datatype support:
%    Currently only double and single precision data types are supported
%    for plotting.
%
%   Modifying path:
%    The directory in which this class lives is automatically added to the
%    MATLAB path.

%   Copyright 2008 The MathWorks, Inc.
%   Author: Navan Ruthramoorthy

  properties (SetAccess = 'private', GetAccess='private')
    FigureHandle         % Handle to main figure window
    AxesHandles          % Handles to each axes. Each channel is in one axis
    ChannelHandles       % Handles to each channel's line plots
    Filename = which('speech_dft.wav');
    AudioData            % Audio data being edited
    iOutliers            % Used for down sampled plot
    jOutliers            % Used for down sampled plot
    Fs = 8000;           % Sample time
    FileReaders = [];    % File reader plugins
    FileFilterSpec = {}; % Spec used to filter files for uigetfile
    SelectorLines        % The lines used to select audio data in the editor
    SelectorPatchHandle  % Highlighting the area which is selected
    RecentFiles = {};    % Most recently used files
    RecentFilesMenu      % Handle to most recent used files menu
    AudioPlayer          % audioplayer object.
    Analyzer             % Object which manages all analyzers
    hFilterManager       % Object which manages all filters
    hFileWriterManager   % Object which manages all file writers
    UndoDataManager      % Manages undo and redo data
    Clipboard = ClipboardManager.getInstance; % Cut copy paste
  end

  properties (Constant = true, SetAccess = 'private', GetAccess='private')
    MaxRecentFiles = 4;  % Maximum number of files in most recent files list
    PreferenceGroupName = 'AudioEditor_ApplicationPrefs';
                         % group name for storing preferences
  end

  methods
    function this = AudioEditor(varargin)
      addpath(fileparts(mfilename('fullpath')));
      this.UndoDataManager = UndoManager;
      createFigure(this);
    end
  end

  methods (Access = 'private')

    function createFigure(this)
      fPos = [360,500,850,350];
      this.FigureHandle = figure( ...
                    'Visible','off', ...
                    'Menubar','none', ...
                    'Toolbar','figure', ...
                    'Position', fPos, ...
                    'IntegerHandle', 'off', ...
                    'Color',    get(0, 'defaultuicontrolbackgroundcolor'), ...
                    'NumberTitle', 'off', ...
                    'Name', 'Audio Editor', ...
                    'WindowButtonUpFcn', @(hobj, evd) figureButtonUpCallback(this), ...
                    'ResizeFcn', @(hObj, evd) resizeFcnCallback(this), ...
                    'CloseRequestFcn', @(hObj, evd) figureCloseCallback(this));

      hZoom = zoom(this.FigureHandle);
      hPan  = pan(this.FigureHandle);
      zoomFcn = @(hObject, eventdata) zoomCallback(this, hObject, eventdata);
      set(hZoom, 'Motion', 'horizontal', ...
                 'ActionPostCallback', zoomFcn);
      set(hPan , 'ActionPostCallback', zoomFcn);

      adjustFigure(this);
      this.AxesHandles = axes( ...
                  'Units','pixels', ...
                  'Box', 'on', ...
                  'ButtonDownFcn', @(hobj, evd) axesButtonDownCallback(this));
      hold on;

      set(this.FigureHandle, 'Name', ['Audio Editor: ' this.Filename]);
      movegui(this.FigureHandle, 'center');
      set(this.FigureHandle,'Visible','on');

      loadFileReaders(this);
      loadAudioFile(this, this.Filename); % default audio file
    end

    % Scan +filereaders directory for file reading objects
    function loadFileReaders(this)
      this.FileFilterSpec{1, 1} = '*.*';
      this.FileFilterSpec{1, 2} = 'All Files (*.*)';
      thisFileDir = fileparts(mfilename('fullpath'));
      readerPackage = 'filereaders';
      readers = what([thisFileDir '/+' readerPackage]);
      if isempty(readers), return, end
      if isempty(this.FileReaders)
        this.FileReaders = struct('Reader', [], 'Ext', {});
      end
      for i=1:length(readers.m)
        try
          h = eval([readerPackage '.' readers.m{i}(1:end-2)]);
          ext = h.getFileExtensions();
          if ~isempty(ext)
            this.FileReaders(end+1) = struct('Reader', h, 'Ext', {ext});
          end
          spec = '';
          for j=1:length(ext)
            spec = [spec '*.' ext{j} ';']; %#ok<AGROW>
          end
          this.FileFilterSpec{end+1, 1} = spec;
          this.FileFilterSpec{end, 2} = ''; % For protection if next call fails
          this.FileFilterSpec{end, 2} = h.getExtDescription();
        catch me
          warning(me.identifier, me.message);
        end
      end
    end

    % Things which need to be emptied when reading a new file
    function clearHandles(this)
      this.AxesHandles = [];
      this.ChannelHandles = [];
      this.SelectorLines = [];
      this.SelectorPatchHandle = [];
      this.UndoDataManager.clearUndoData();
    end

    % updates audio data in the plots. Also called for zoom callbacks.
    function updatePlot(this, range)
      range = floor(range * this.Fs);
      numPoints = 50000;
      len = length(this.ChannelHandles);
      x = (1:size(this.AudioData, 1))';
      id = find(x >= range(1) & x <= range(2));
      if length(id) > numPoints/len
        idx = round(linspace(id(1), id(end), round(numPoints/len)))';
        ol = this.iOutliers > id(1) & this.iOutliers < id(end);
        for i=1:len
          idxi = unique([idx; this.iOutliers(ol & this.jOutliers == i)]);
          set(this.ChannelHandles(i), 'XData', idxi/this.Fs, ...
                                      'YData', this.AudioData(idxi));
        end
      else
        for i=1:len
          set(this.ChannelHandles(i), 'XData', id/this.Fs, ...
                                      'YData', this.AudioData(id));
        end
      end
    end

    % Plots downsampled data. This has much faster response time. This is
    % based on dsplot by Jiro Doke in file exchange.
    % TODO Add data cursor callbacks
    function dsplot(this)
      len = length(this.AudioData);
      x = 1:len;
      % Find outliers
      filterWidth = min([50, ceil(length(x)/10)]); % max window size of 50
      a  = this.AudioData - ...
                filter(ones(filterWidth,1)/filterWidth, 1, this.AudioData);
      [this.iOutliers, this.jOutliers] = ...
             find(abs(a - repmat(mean(a), size(a, 1), 1)) > ...
                    repmat(4 * std(a), size(a, 1), 1));
      updatePlot(this, [0 len/this.Fs]);
    end

    function loadAudioFile(this, filename)
      [data, Fs, errFlag] = readAudioFile(this, filename);
      if ~errFlag
        this.Filename = filename;
        plotData(this, data, Fs);
        addToRecentFiles(this, this.Filename);
      end
    end

    % Plot audio data.
    function plotData(this, data, Fs)
      this.AudioData = data;
      this.Fs = Fs;
      sz = size(this.AudioData);
      clearHandles(this);
      set(this.FigureHandle, 'HandleVisibility', 'on');
      for i=1:sz(2)
        subplot(sz(2), 1, i),
        this.ChannelHandles(i) = plot(nan);   % Get a handle for plot
        this.AxesHandles(i) = get(this.ChannelHandles(i), 'Parent');
        set(this.AxesHandles(i), ...
              'ButtonDownFcn', @(hobj, evd) axesButtonDownCallback(this), ...
              'Units', 'Pixels');
        set(this.ChannelHandles(i), ...
              'ButtonDownFcn', @(hobj, evd) axesButtonDownCallback(this));
        axis([0 sz(1)/this.Fs -1 1]);
        zoom reset;     % Remember original zoom position
      end
      dsplot(this);
      addSelectorTool(this);
      set(this.FigureHandle, 'Name', ['Audio Editor: ' this.Filename], ...
                             'HandleVisibility', 'off');
      % If the number of axes changes we need to resize the axes
      resizeFcnCallback(this);
      linkaxes(this.AxesHandles); % link axes for zooming
    end

    % Read audio data from given file.
    function [data, Fs, errFlag] = readAudioFile(this, filename)
      reader = findReader(this, filename);
      if ~isempty(reader)
        try
            setFilename(reader, filename);
            data = getData(reader);
            Fs = getSampleRate(reader);
            errFlag = 0;
        catch ME
            errordlg(ME.message, 'AudioEditor:FileReader');
            data = [];
            Fs = [];
            errFlag = 1;
        end
      else
        data = [];
        Fs = [];
        errFlag = 1;
      end
    end

    % Find the audio file reader which will read the given filename.
    % The search is based on the filename extension.
    function reader = findReader(this, filename)
      reader = [];
      [p,n,ext] = fileparts(filename);
      ext = ext(2:end); % Remove .
      for i=1:length(this.FileReaders)
        if ismember(ext, this.FileReaders(i).Ext)
          reader = this.FileReaders(i).Reader;
          break;
        end
      end
      if isempty(reader)
        errordlg('This file extension is not supported.', ...
                 'AudioEditor:cantFindReader');
      end
    end

    function createToolbar(this)
      audiotb = uitoolbar(this.FigureHandle);
      addPlaybackControls(this, audiotb);

      tth = findall(this.FigureHandle, 'Type', 'uitoolbar');
      ttb = findall(tth, 'Type', 'uipushtool');

      % Set callback for file open button
      fo = findobj(ttb, 'Tag', 'Standard.FileOpen');
      set(fo, 'Separator', 'on', 'ClickedCallback', ...
              @(hobj, evd) fileOpenCallback(this));
      copyobj(fo, audiotb);

      % copy standard buttons to our toolbar
      fo = findobj(ttb, 'Tag', 'Standard.SaveFigure');
      set(fo, 'ClickedCallback', @(hobj, evd) fileWriteCallback(this));
      copyobj(fo, audiotb);

      copyobj(findobj(ttb, 'Tag', 'Standard.PrintFigure'), audiotb);

      ttb = findall(tth, 'Type', 'uitoggletool');
      copyobj(findobj(ttb, 'Tag', 'Exploration.ZoomIn'), audiotb);
      copyobj(findobj(ttb, 'Tag', 'Exploration.ZoomOut'), audiotb);
      copyobj(findobj(ttb, 'Tag', 'Exploration.DataCursor'), audiotb);

      % Add zoom to selection button
      [cd,m,alpha] = imread('icons/ae_zoom_select.png');
      cd = double(cd) / 255;
      cd(~alpha) = NaN;
      uipushtool(audiotb, 'CData', cd, ...
           'TooltipString', 'Fit selection to axes', ...
           'HandleVisibility','off',...
           'ClickedCallback', @(hobj, evd) zoomSelectionCallback(this));

      % Add zoom out back to full signal button
      [cd,m,alpha] = imread('icons/ae_zoom_outfull.png');
      cd = double(cd) / 255;
      cd(~alpha) = NaN;
      uipushtool(audiotb, 'CData', cd, ...
           'TooltipString', 'Fit entire signal to axes', ...
           'HandleVisibility','off',...
           'ClickedCallback', @(hobj, evd) zoomOutfullCallback(this));

      % Move data cursor button to the end
      ttc = allchild(audiotb);
      ttc(1:3) = ttc([3 1 2]);
      set(audiotb, 'Children', ttc);
      set(this.FigureHandle, 'Toolbar', 'none');
    end

    function createMenubar(this)
      mhData = uimenu(this.FigureHandle, 'Label', 'Data');
      uimenu(mhData, 'Label', 'Read From File...', 'Callback', ...
             @(hobj, evd) fileOpenCallback(this));
      uimenu(mhData, 'Label', 'Write To File...', 'Callback', ...
             @(h, evd) fileWriteCallback(this));
      uimenu(mhData, 'Label', 'Read From MATLAB...', 'Callback', ...
             @(h, evd) fromMATLABCallback(this));
      uimenu(mhData, 'Label', 'Write To MATLAB...', 'Callback', ...
             @(h, evd) toMATLABCallback(this));
      uimenu(mhData, 'Label', 'Record From Mic...', 'Callback', ...
             @(h, evd) fromMicCallback(this));
      this.RecentFilesMenu = uimenu(mhData, 'Label', 'Recent Files');
      recentfilenames = getpref(this.PreferenceGroupName, ...
                                'RecentFilesList', which('speech_dft.wav'));
      addToRecentFiles(this, recentfilenames);
      mhEdit = uimenu(this.FigureHandle, 'Label', 'Edit');
      hUndo = uimenu(mhEdit, 'Label', 'Undo', 'Callback',...
           @(hobj, evd) undoCallback(this));
      hRedo = uimenu(mhEdit, 'Label', 'Redo', 'Callback',...
           @(hobj, evd) redoCallback(this));
      this.UndoDataManager.setUndoRedoMenuHandles(hUndo, hRedo);
      hSelectAll = uimenu(mhEdit, 'Label', 'Select All', ...
                    'Callback', @(hobj, evd) selectAllCallback(this));
      set(hSelectAll, 'Separator', 'on');
      hcutMenu = uimenu(mhEdit, 'Label', 'Cut', ...
                    'Callback', @(hobj, evd) cutCallback(this));
      set(hcutMenu, 'Separator', 'on');
      uimenu(mhEdit, 'Label', 'Copy', ...
                    'Callback', @(hobj, evd) copyCallback(this));
      uimenu(mhEdit, 'Label', 'Paste', ...
                    'Callback', @(hobj, evd) pasteCallback(this));
      uimenu(mhEdit, 'Label', 'Paste Add', ...
                    'Callback', @(hobj, evd) pasteAddCallback(this, hobj));
      uimenu(mhEdit, 'Label', 'Paste Substract', ...
                    'Callback', @(hobj, evd) pasteAddCallback(this, hobj));
      createFilterMenu(this);
      createAnalyzeMenu(this);
    end

    function createAnalyzeMenu(this)
      hAnalyze = uimenu(this.FigureHandle, 'Label', 'Analyze');
      names = loadAnalyzers(this);
      for i=1:length(names)
        uimenu(hAnalyze, 'Label', names{i}, 'Callback', ...
               @(obj, eventdata) analyzerCallback(this, obj));
      end
    end
  
    function names = loadAnalyzers(this)
      this.Analyzer = AnalysisManager;
      names = this.Analyzer.loadAnalyzers();
    end

    function createFilterMenu(this)
      hFilter = uimenu(this.FigureHandle, 'Label', 'Filter');
      names = loadFilters(this);
      for i=1:length(names)
        uimenu(hFilter, 'Label', names{i}, 'Callback', ...
               @(obj, eventdata) filterCallback(this, obj));
      end
    end

    function names = loadFilters(this)
      this.hFilterManager = FilterManager;
      names = this.hFilterManager.loadFilters();
    end

    function addToRecentFiles(this, filenames)
      if ~iscell(filenames)
        filenames = {filenames};
      end
      % Update file list
      for i=1:length(filenames)
        [found, loc] = ismember(filenames{i}, this.RecentFiles);
        if found % Move to the top if already on the list
          this.RecentFiles = {this.RecentFiles{loc} this.RecentFiles{(1:end)~=loc}};
        else
          if length(this.RecentFiles) < this.MaxRecentFiles % add to end
            this.RecentFiles(2:end+1) = this.RecentFiles(1:end);
          else % cycle
            this.RecentFiles(2:end) = this.RecentFiles(1:end-1);
          end
          this.RecentFiles(1) = filenames(i);
         end
      end
      % Update menu
      delete(get(this.RecentFilesMenu, 'Children')); % delete existing menu
      for i=1:length(this.RecentFiles)
        [p, n, e] = fileparts(this.RecentFiles{i});
        uimenu(this.RecentFilesMenu, 'Label', [n e], 'UserData', i, ...
               'Callback', @(hobj, evd) recentFilesCallback(this, hobj));
      end
    end

    function adjustFigure(this)
      createToolbar(this);
      createMenubar(this);
    end

    % Add audio play back controls.
    function addPlaybackControls(this, parent)
      % Add playback controls
      load audiotoolbaricons;
      uipushtool(parent, 'CData', play_on,...
                   'TooltipString', 'Play',...
                   'HandleVisibility','off',...
                   'ClickedCallback', @(hobj, evd) playCallback(this));
      uipushtool(parent, 'CData', pause_default,...
                   'TooltipString', 'Pause',...
                   'HandleVisibility','off',...
                   'ClickedCallback', @(hobj, evd) pauseCallback(this));
      uipushtool(parent, 'CData', stop_default,...
                   'TooltipString', 'Stop',...
                   'HandleVisibility','off',...
                   'ClickedCallback', @(hobj, evd) stopCallback(this));
    end

    % Add the selector lines and the gray selection highlight area.
    function addSelectorTool(this)
      cbFcn = @(hobj, evd) selectButtonDownCallback(this, hobj);
      for i=1:length(this.AxesHandles)
        axes(this.AxesHandles(i));
        this.SelectorLines(1, i) = line([0 0], [-1 1], ...
                                        'ButtonDownFcn', cbFcn, ...
                                        'Parent', this.AxesHandles(i));
        this.SelectorLines(2, i) = line([0 0], [-1 1], ...
                                        'ButtonDownFcn', cbFcn, ...
                                        'Parent', this.AxesHandles(i));
        this.SelectorPatchHandle(i) = patch([0 0 0 0], [-1 1 1 -1], ...
                                  [0.5 0.5 0.5], 'FaceAlpha', 0.5, ...
                                  'EdgeColor', [1 0 0], ...
                                  'HitTest', 'off', ...
                                  'Parent', this.AxesHandles(i));
      end
    end

    % Return the points where the selector lines are.
    % The first point is always smaller.
    function [xd1, xd2] = getSelectionPoints(this)
      xd1 = get(this.SelectorLines(1), 'XData');
      xd2 = get(this.SelectorLines(2), 'XData');
      if xd1(1) > xd2(1) % Make xd1 the smaller
        temp = xd1;
        xd1 = xd2;
        xd2 = temp;
      end
    end

    function [xd1, xd2] = getSelectionSampleNumbers(this)
      [xd1, xd2] = getSelectionPoints(this);
      xd1 = floor(xd1*this.Fs);
      xd2 = floor(xd2*this.Fs);
    end

    function stopAudioPlayer(this)
      if ~isempty(this.AudioPlayer)
        stop(this.AudioPlayer); % StopFcn is not called when player is paused
        h = this.AudioPlayer.UserData;
        if ishandle(h)
          delete(h);
        end
      end
    end

    function data = getCurrentData(this, x1, x2)
      if x1 ~= x2
        % Get selected data
        data = this.AudioData(x1(1)+1:x2(1), :);
      else
        % Get 2048 samples around cursor
        % 1048 on the left side and 1047 on the right. If the cursor is
        % close to the extremes the first or the last 2048 samples are
        % returned.
        audioLen = size(this.AudioData, 1);
        if x1 - 1024 >= 1 && x1+1024 < audioLen
          data = this.AudioData(x1-1024:x1+1023, :);
        elseif x1 - 1024 >= 1 && audioLen >= 2048
          data = this.AudioData(end-2047:end, :);
        elseif x1+1024 < audioLen && audioLen >= 2048
          data = this.AudioData(1:2048, :);
        else % audioLen < 2048
          data = [this.AudioData; zeros(2048-audioLen ,size(this.AudioData, 2))];
        end
      end
    end

  end % Private methods

  methods (Access = 'public', Hidden = true) % Callback methods

    function resizeFcnCallback(this)
      fPos = get(this.FigureHandle, 'Position');
      len = length(this.AxesHandles);
      h = round((fPos(4)-45-5*len)/len); % Height of each axis
      for i=0:len-1
        set(this.AxesHandles(len-i), 'Position', [35 45+h*i+5*i fPos(3)-60 h]);
        set(this.AxesHandles(len-i), 'plotBoxAspectRatio', [(fPos(3)-60)/h 1 1]);
        % Setting PlotBoxAspectRatio keeps the axis box on
        if i ~= 0
          % Need XTicks only for bottom axis
          set(this.AxesHandles(len-i), 'XTick', []);
        end
      end
    end

    function fileOpenCallback(this)
      [filename, pathname] = ...
               uigetfile(this.FileFilterSpec, 'Audio file to read', ...
                         this.Filename);
      if ~(isequal(filename,0) || isequal(pathname,0))
        loadAudioFile(this, [pathname filename]);
      end
    end
    
    function fromMicCallback(this)
      [y, Fs] = RecordFromMic;
      if ~isempty(y) && ~isempty(Fs)
        this.Filename = 'From Microphone';
        plotData(this, y, Fs);
      end
    end

    function fileWriteCallback(this)
      if isempty(this.hFileWriterManager)
         this.hFileWriterManager = FileWriterManager;
      end
      fileName = this.hFileWriterManager.writeFile(this.AudioData, this.Fs);
      if ~isempty(fileName)
        addToRecentFiles(this, fileName);
      end
    end

    function fromMATLABCallback(this)
      [varName, Fs] = getFromMATLABVarNameandFs();
      if ~isempty(varName)
        this.Filename = varName;
        plotData(this, evalin('base', varName), Fs);
      end
    end

    function toMATLABCallback(this)
      [varName, FsName] = getToMATLABVarNameandFs();
      if ~isempty(varName) && ~isempty(FsName)
        if ~isvarname(varName) || ~isvarname(FsName)
          errordlg('Invalid variable name.', 'Audio Editor: Error');
        end
        assignin('base', varName, this.AudioData);
        assignin('base', FsName, this.Fs);
      end
    end

    function recentFilesCallback(this, hObject)
      fileIdx = get(hObject, 'UserData');
      fileName = this.RecentFiles{fileIdx};
      if ~exist(fileName, 'file')
          errordlg(['The file ' fileName 'does not exist in path.'], ...
                   'AudioEditor:cantFindFile');
      end
      loadAudioFile(this, fileName);
    end

    % Play audio
    function playCallback(this)
      stopAudioPlayer(this);
      [xd1, xd2] = getSelectionSampleNumbers(this);
      if xd1(1) == xd2(1)
        xd2(1) = size(this.AudioData, 1);
      end
      hl = zeros(length(this.AxesHandles), 1);
      for i=1:length(this.AxesHandles)
        axes(this.AxesHandles(i)); % Make this current axis
        hl(i) = line([xd1(1) xd1(1)]/this.Fs, [-1 1], 'Color', [1 0 0], ...
                     'Parent', this.AxesHandles(i));
      end
      this.AudioPlayer = audioplayer(this.AudioData(xd1(1)+1:xd2(1), :), this.Fs);
      set(this.AudioPlayer, 'TimerPeriod', 0.05, 'UserData', hl, ...
          'TimerFcn', @(ap, evd) audioPlayerCallbackTimer(this, ap, hl, xd1(1)), ...
          'StopFcn',  @(ap, evd) audioPlayerCallbackStop(this, ap, hl));
      play(this.AudioPlayer);
    end

    % Pause audio playback
    function pauseCallback(this)
      if ~isempty(this.AudioPlayer)
        if isplaying(this.AudioPlayer)
          pause(this.AudioPlayer);
        elseif this.AudioPlayer.CurrentSample ~= 1
          % resume only if it is paused
          resume(this.AudioPlayer);
        end
      end
    end

    % Stop audio playback
    function stopCallback(this)
      stopAudioPlayer(this);
    end

    % Position both selection lines at the mouse click point
    function axesButtonDownCallback(this)
      maxxd = size(this.AudioData, 1)/this.Fs;
      cp = get(this.AxesHandles(1), 'CurrentPoint');
      if cp(1) > maxxd || cp(1) < 0
          return;
      end
      if strcmp(get(this.FigureHandle,'SelectionType'),'normal')
        % Move selector lines to this point
        set(this.SelectorLines, 'XData', [cp(1) cp(1)]);
        set(this.SelectorPatchHandle, 'XData', [cp(1) cp(1) cp(1) cp(1)]);
        hLine = this.SelectorLines(1,:);
        set(this.FigureHandle, 'WindowButtonMotionFcn', ...
            @(hobj, evd) selectLineDragCallback(this, hLine));
      elseif strcmp(get(this.FigureHandle,'SelectionType'),'alt') % right click
        xd1 = get(this.SelectorLines(1), 'XData');
        xd2 = get(this.SelectorLines(2), 'XData');
        % Find closest line and move that to this point
        if abs(xd1(1)-cp(1)) < abs(xd2(1)-cp(1))
          set(this.SelectorLines(1,:), 'XData', [cp(1) cp(1)]);
          set(this.SelectorPatchHandle, 'XData', [cp(1) cp(1) xd2(1) xd2(1)]);
        else
          set(this.SelectorLines(2,:), 'XData', [cp(1) cp(1)]);
          set(this.SelectorPatchHandle, 'XData', [xd1(1) xd1(1) cp(1) cp(1)]);
        end
      end
    end

    % Enable dragging of selection lines at the mouse click point
    function selectButtonDownCallback(this, hObject)
      if strcmp(get(this.FigureHandle,'SelectionType'),'normal')
        [r, c] = find(this.SelectorLines == hObject); %#ok<NASGU>
        hLine = this.SelectorLines(1,:);
        set(this.FigureHandle, 'WindowButtonMotionFcn', ...
            @(hobj, evd) selectLineDragCallback(this, hLine));
      end
    end

    % Drag selection lines
    function selectLineDragCallback(this, hLine)
      if strcmp(get(this.FigureHandle,'SelectionType'),'normal')
        maxxd = size(this.AudioData, 1)/this.Fs;
        cp = get(this.AxesHandles(1), 'CurrentPoint');
        if cp(1,1) > maxxd
          cp(1,1) = maxxd;
        elseif cp(1,1) < 0
          cp(1,1) = 0;
        end
        set(hLine, 'XData', [cp(1,1) cp(1,1)]);
        xd = zeros(1, 4);
        xd(1:2) = get(this.SelectorLines(1), 'XData');
        xd(3:4) = get(this.SelectorLines(2), 'XData');
        set(this.SelectorPatchHandle, 'XData', xd);
      end
    end

    function selectAllCallback(this)
      set(this.SelectorLines(1, :), 'XData', [0 0]);
      len = size(this.AudioData, 1)/this.Fs;
      set(this.SelectorLines(2, :), 'XData', [len len]);
      set(this.SelectorPatchHandle, 'XData', [0 0 len len]);
    end

    % Stop selection drag
    function figureButtonUpCallback(this)
      set(this.FigureHandle, 'WindowButtonMotionFcn', '');
      [xd1, xd2] = getSelectionSampleNumbers(this);
      data = getCurrentData(this, xd1(1), xd2(1));
      this.Analyzer.analyze(data, this.Fs);
    end

    % cut
    function cutCallback(this)
      [xd1, xd2] = getSelectionSampleNumbers(this);
      this.Clipboard.Data = this.AudioData(xd1(1)+1:xd2(1), :);
      this.AudioData(xd1(1)+1:xd2(1), :) = [];
      dsplot(this);
      this.UndoDataManager.pushData(this.Clipboard.Data, ...
                    [], xd1(1), xd2(1), xd1(1));
      xd1 = xd1 / this.Fs;
      set(this.SelectorLines, 'XData', xd1);
      set(this.SelectorPatchHandle, 'XData', [xd1(1) xd1(1) xd1(1) xd1(1)]);
    end

    % copy
    function copyCallback(this)
      [xd1, xd2] = getSelectionSampleNumbers(this);
      this.Clipboard.Data = this.AudioData(xd1(1)+1:xd2(1), :);
    end

    % paste
    function pasteCallback(this)
      [xd1, xd2] = getSelectionSampleNumbers(this);
      this.UndoDataManager.pushData(this.AudioData(xd1(1)+1:xd2(1), :), ...
                                    this.Clipboard.Data, ...
                                    xd1(1), xd2(1),...
                                    xd1(1)+size(this.Clipboard.Data,1));
      this.AudioData = [this.AudioData(1:xd1(1), :); ...
                        this.Clipboard.Data; ...
                        this.AudioData(xd2(1)+1:end, :)];
      dsplot(this);
      xd1 = xd1 / this.Fs;
      xd2 = xd1 + size(this.Clipboard.Data, 1)/this.Fs;
      if get(this.SelectorLines(1), 'XData') <  get(this.SelectorLines(2), 'XData')
        set(this.SelectorLines(2,:), 'XData', xd2);
      else
        set(this.SelectorLines(1,:), 'XData', xd2);
      end
      set(this.SelectorPatchHandle, 'XData', [xd1(1) xd1(1) xd2(1) xd2(1)]);
    end

    % paste add
    function pasteAddCallback(this, hobj)
      [xd1, xd2] = getSelectionSampleNumbers(this);
      xd2(1) = xd1(1) + size(this.Clipboard.Data, 1);
      if xd2(1) > size(this.AudioData, 1)
          xd2(1) = size(this.AudioData, 1);
      end
      this.UndoDataManager.pushData(this.AudioData(xd1(1)+1:xd2(1), :), ...
                                    this.Clipboard.Data, ...
                                    xd1(1), xd2(1),...
                                    xd1(1)+size(this.Clipboard.Data,1));
      if strcmp(get(hobj, 'Label'), 'Paste Add')
          this.AudioData(xd1(1)+1:xd2(1), :) = ...
                        this.AudioData(xd1(1)+1:xd2(1), :) +...
                        this.Clipboard.Data(1:(xd2(1)-xd1(1)), :);
      else
          this.AudioData(xd1(1)+1:xd2(1), :) = ...
                        this.AudioData(xd1(1)+1:xd2(1), :) -...
                        this.Clipboard.Data(1:(xd2(1)-xd1(1)), :);
      end
      dsplot(this);
    end

    % undo
    function undoCallback(this)
      [data, x1, x2] = this.UndoDataManager.getUndoData();
      if ~isempty(x1)
        this.AudioData = [this.AudioData(1:x1(1), :); data; ...
                          this.AudioData(x2(1)+1:end, :)];
      end
      dsplot(this);
    end

    % redo
    function redoCallback(this)
      [data, x1, x2] = this.UndoDataManager.getRedoData();
      if ~isempty(x1)
        this.AudioData = [this.AudioData(1:x1(1), :); data; ...
                          this.AudioData(x2(1)+1:end, :)];
      end
      dsplot(this);
    end

    % zoom
    function zoomCallback(this, hFig, eventdata)
      switch get(hFig, 'SelectionType')
        case {'normal', 'alt'}
          xlimits = xlim(eventdata.Axes);
        case 'open'
          xlimits = [0 size(this.AudioData, 1)/this.Fs];
      end
      updatePlot(this, xlimits);
    end

    % Callback for fit selection to the axis button
    function zoomSelectionCallback(this)
      [xd1, xd2] = getSelectionPoints(this);
      if xd1(1) ~= xd2(1)
        updatePlot(this, [xd1(1) xd2(1)]);
        set(this.AxesHandles, 'XLim', [xd1(1) xd2(1)]);
      end
    end

    % Callback for fit entire signal to the axis button
    function zoomOutfullCallback(this)
      xlimits = [0 size(this.AudioData, 1)/this.Fs];
      updatePlot(this, xlimits);
      set(this.AxesHandles, 'XLim', xlimits);
    end

    % Callback from analysis menu
    function analyzerCallback(this, obj)
      aName = get(obj, 'Label');
      [xd1, xd2] = getSelectionSampleNumbers(this);
      data = getCurrentData(this, xd1(1), xd2(1));
      this.Analyzer.analyze(data, this.Fs, aName);
    end

    function filterCallback(this, obj)
      [xd1, xd2] = getSelectionSampleNumbers(this);
      x1 = xd1(1); x2 = xd2(1);
      if x1 ~= x2
        % Send selected data to filter
        y = this.hFilterManager.filter(get(obj, 'Label'), ...
                            this.AudioData(x1+1:x2, :), this.Fs);
        this.UndoDataManager.pushData(this.AudioData(xd1(1)+1:xd2(1), :), ...
                                      y, xd1(1), xd2(1), xd1(1)+size(y,1));
        % Insert received data
        this.AudioData = [this.AudioData(1:x1,:); y; ...
                          this.AudioData(x2+1:end, :)];
        dsplot(this); % Update plot
        % Update selection
        set(this.SelectorLines(1,:), 'XData', xd1/this.Fs);
        xd2 = xd1 + size(y,1);
        set(this.SelectorLines(2,:), 'XData', xd2/this.Fs);
        set(this.SelectorPatchHandle, 'XData', [xd1, xd2]/this.Fs);
      else
        helpdlg('Data needs to be selected for applying filter.', ...
                'Audio Editor: help');
      end
    end

    function audioPlayerCallbackTimer(this, ap, hl, offset)
      if ~ishandle(hl), return, end
      x = get(ap, 'CurrentSample') + offset;
      set(hl, 'XData', [x x]/this.Fs);
      data = getCurrentData(this, x, x);
      this.Analyzer.analyze(data, this.Fs);
    end

    function audioPlayerCallbackStop(this, ap, hl) %#ok<INUSL>
      % This is also called when player is paused
      if ishandle(hl(1)) && (get(ap, 'CurrentSample') == 1 || ...
         get(ap, 'CurrentSample') == get(ap, 'TotalSamples'))
        delete(hl);
      end
    end

    function figureCloseCallback(this)
      stopAudioPlayer(this);
      setpref(this.PreferenceGroupName, 'RecentFilesList', this.RecentFiles);
      delete(this.FigureHandle);
      delete(this);
    end

  end % public Methods

end
% ---------------------- End of class definition --------------------------

Contact us