MATLAB Answers

User surface on piano

3 views (last 30 days)
Alina Panzel
Alina Panzel on 28 Oct 2019
Commented: Thomas Satterly on 5 Nov 2019
Hey there!
I am new to Matlab and stuck with a problem: I have plotted a piano keyboard with notes for an experimental set up. (Will post it below) now I would like to set up
a user surface where the participants can mouse click onto the keyboard keys as a response which is then saved in a datafile. I'm not sure if we are aloud to use appdeveloper and as far as I understood it it would rather work with an uploaded picture. I've also considered ginput (and sort out the areas afterwards) but that would leave me with a lot of unnecessary data. How do I set up a interactive grids onto each of my keyboard keys?
Thank you so much for anyone whos answering!!
Bildschirmfoto 2019-10-28 um 17.16.56.png

  0 Comments

Sign in to comment.

Answers (3)

Thomas Satterly
Thomas Satterly on 28 Oct 2019
Matlab has button click callbacks for graphical types. There's always multiple ways to approach a problem, but this is the route I'd take:
function testPiano
% Create a figure and axis
fh = figure;
ax = axes;
% Make a button click response for the axis
ax.ButtonDownFcn = @(src, event) mouseClickCallback(src, event);
ax.Color = [0.8 0.8 0.8]; % Make the axis light grey
% Make two rectangular keys out of patches
% Using the "UserData" property to store what key it is here. You could
% also pass extra arguments throught the callback (e.g., a third input)
whiteKey = patch([0 0.5 0.5 0 0], [0 0 1 1 0], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'C1');
blackKey = patch([0.25 0.75 0.75 0.25 0.25], [0.35 0.35 1 1 0.35], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'C#1');
% Make the keys respond to mouse click
whiteKey.ButtonDownFcn = @(src, event) mouseClickCallback(src, event);
blackKey.ButtonDownFcn = @(src, event) mouseClickCallback(src, event);
% Probably want this to make sure the X and Y axis have the same step size
ax.DataAspectRatio = [1 1 1];
function mouseClickCallback(src, event)
% Single callback function, but switching behavior based on the
% source of the callback
switch class(src)
case {'matlab.graphics.primitive.Patch'}
% Patch clicked, so let's respond with the key value we
% stored
fprintf('Clicked key %s\n', src.UserData);
case {'matlab.graphics.axis.Axes'}
% Axis clicked, so let's respond with the (X, Y) position
% that was clicked
fprintf('Clicked axis at point %0.3f, %0.3f\n', src.CurrentPoint(1, 1), src.CurrentPoint(1, 2));
otherwise
fprintf('Unknown object of type ''%s'' clicked\n', class(src));
end
end
end
This demo code uses callbacks from patches and axes to make some kind of unique response. If you're going the "grid" route, you'll have to supply your own logic for detecting which key you're actually over using the mouse position data. As a side note, if you don't want something to intercept mouse click events (like text labels), just turn the "HitTest" property of the object to "off".

  4 Comments

Show 1 older comment
Alina Panzel
Alina Panzel on 29 Oct 2019
Just to check the vectors are supposed to be on top of every key so I'll have to put new vector values in and add one white key for every white key and so on..
'patch(X,Y,C) adds the filled two-dimensional patch to the current axes. The elements of X and Y specify the vertices of a polygon. If X and Y are matrices, MATLAB draws one polygon per column. C determines the color of the patch. It can be a single ColorSpec, one color per face, or one color per vertex (see "Remarks"). If C is a 1-by-3 vector, it is assumed to be an RGB triplet, specifying a color directly.'
Is it C ? and if so, why would it be 0.25 on x and 0.35 on y in the black key, wouldnt it be the same?
Thomas Satterly
Thomas Satterly on 29 Oct 2019
Regarding a X and Y being 5 elements instead of 4 - that's because I made a mistake (must have been thinking in closing the loop myself). The first four points are all that's needed.
The 0.25 on X and 0.35 on Y was just to offset the rectangle to the right and move the bottom edge up to look more like where a black key should be - but purely made up numbers. You'll probably want to make a couple loops to draw each white and black key. One more note: the draw order will stack the last created patch on top, but you can use "uistack" to force the draw order of a graphics object after it's created, but if you draw all white keys first, then all black keys, you won't have to adjust the order.
Alina Panzel
Alina Panzel on 29 Oct 2019
Perfect than I've done everything right I've created new 4 object vectors and created every white and then all the black keys :) thank you so much!

Sign in to comment.


Alina Panzel
Alina Panzel on 30 Oct 2019
Dear Thomas,
Unfortunatly I have another problem, I managed to make everythign work for every key in the keyboard but when I started moving on to saving it in the text file it started writing itself over instead of adding data into the textfile. Also, I had to put the fileID etc into the function in the bottom otherwise it did not find it although I tried to make it global. That leaves me wondering if there is an element in the function below that interefering with the data collection ? What should I be looking for?
Looking forward to hearing from you!
Alina

  1 Comment

Thomas Satterly
Thomas Satterly on 30 Oct 2019
Could you post your code? Without seeing the code, I would ensure that you are only using fopen once, or if that is not possible, make sure you are using the permission 'a+' to append to the existing file rather than overwriting it.

Sign in to comment.


Alina Panzel
Alina Panzel on 5 Nov 2019
% Create a figure and axis
fh = figure;
ax = axes;
% creates a button click response for the axis
ax.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
%
ax.Color = [0.8 0.8 0.8]; % Makes the axis light grey
% Make two rectangular keys out of patches
% Using "UserData" to store what key it is .
% extra arguments throught the 'callback' (e.g., a third input)? Look up
% again
whiteKey1 = patch([0 1 1 0], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'C1');
whiteKey2 = patch([1 2 2 1], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'D1');
whiteKey3 = patch([2 3 3 2], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'E1');
whiteKey4 = patch([3 4 4 3], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'F1');
whiteKey5 = patch([4 5 5 4], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'G1');
whiteKey6 = patch([5 6 6 5], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'A1');
whiteKey7 = patch([6 7 7 6], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'H1');
whiteKey8 = patch([7 8 8 7], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'C2');
whiteKey9 = patch([8 9 9 8], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'D2');
%111is white,000is black
blackKey1 = patch([0.6 1.4 1.4 0.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'C#1');
blackKey2 = patch([1.6 2.4 2.4 1.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'D#1');
blackKey3 = patch([3.6 4.4 4.4 3.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'F#1');
blackKey4 = patch([4.6 5.4 5.4 4.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'G#1');
blackKey5 = patch([5.6 6.4 6.4 5.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'A#1');
blackKey6 = patch([7.6 8.4 8.4 7.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'C#2');
%same here, putting it in seperate names to avoid overwriting
% creates letters
text(0.4,1.5,'C')
text(1.4,1.5,'D')
text(2.4,1.5,'E')
text(3.4,1.5,'F')
text(4.4,1.5,'G')
text(5.4,1.5,'A')
text(6.4,1.5,'H')
text(7.4,1.5,'C')
text(8.4,1.5,'D')
%creates smaller, white letters on black keys
text(0.85,4,'C#','Color','white','FontSize',9)
text(1.85,4,'D#','Color','white','FontSize',9)
text(3.85,4,'F#','Color','white','FontSize',9)
text(4.85,4,'G#','Color','white','FontSize',9)
text(5.85,4,'A#','Color','white','FontSize',9)
text(7.85,4,'C#','Color','white','FontSize',9)
text(1.5,0,'Now click the matching key on the piano')
% Makes the keys respond to mouse click
whiteKey1.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey2.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey3.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey4.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey5.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey6.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey7.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey8.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey9.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey1.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey2.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey3.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey4.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey5.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey6.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
% makes sure the X and Y axis have the same step size; not sure if needed
% seen somewhere
ax.DataAspectRatio = [1 1 1];
function ClickresponseCallback(src, event)
% exp_directory = "/Users/alinapanzel/Documents/MATLAB/result.txt";
% fileID = fopen(exp_directory'w');
% callback function, but switching behavior based on the source of the callback
%Mathworks
switch class(src)
case {'matlab.graphics.primitive.Patch'}
%this is from a mathworks community member
% Patch clicked, so let's respond with the key value we
% stored fprintf writes data into a textfile
%first input is a string of what will be displayed in the command window
% %d = decimal notation
% \n = new line \t = tab space \
% second input assigns decimal(%d) in the string of value
%this is from a mathworks community member
exp_directory = "/Users/alinapanzel/Documents/MATLAB/result.txt";
fileID = fopen(exp_directory,'w','a');
fprintf(fileID, 'Clicked key\n\n');
fprintf(fileID,'%f %f\n','a+', src.UserData);
fclose(fileID);
type result.txt
%fprintf('Clicked key %s\n', src.UserData);
%fprintf(fileID 'Clicked key %s\n', src.UserData);
%case {'matlab.graphics.axis.Axes'}
% Axis clicked, so let's respond with the (X, Y) position
% that was clicked
%this is from a mathworks community member
%fprintf('Clicked axis at point %0.3f, %0.3f\n', src.CurrentPoint(1, 1), src.CurrentPoint(1, 2));
%otherwise
%fprintf('Unknown object of type ''%s'' clicked\n', class(src));
end
end
It saves the first click and adds a second but afterwards it just starts writing itself over and over :/
Thank you so much for your help so far, you've teached me a lot already :)

  1 Comment

Thomas Satterly
Thomas Satterly on 5 Nov 2019
You're pretty close, but file IO can be difficult to manage. Your current fopen statement has both 'w' and 'a' permissions on it - that's not really how you specify permissions, it should just be a single input. You could fix it by just using fopen(fileName, 'a') so that it only ever appends to the file if it already exists. Also, on your print statement, you should be using the format specification '%s' to signify that you are printing a string, not a number. When you have '%f' as a format spec, Matlab is converting over each character in your string to their number, which I don't think is what you want.
What I'd propose is moving towards a file ID that stays open for the duration of the script. This way, you won't be constantly opening and closing (which is generally a bit safer), and it will open you up to some more capabilities of Matlab.
First, in order to share a variable across multiple locations within a script, you can either use global variables or persistent variables hidden inside a function. Don't use globals, they are bad practice. Hiding a variable as a persistent variable inside of a function takes a little effort, but is much safer (see the getFileID() function).
Next comes how to close the file when you're done. You can take advantage of a figure's CloseRequestFcn callback (similar to a ButtonDownFcn callback) that executes when the figure is closed. Inside of this function, the file ID is retrived, closed, and then the figure itself deleted so that it actually closes.
These changes are reflected in the code below, along with a few more notes on persistent variables
% Create a figure and axis
fh = figure;
ax = axes;
% creates a button click response for the axis
ax.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
%
ax.Color = [0.8 0.8 0.8]; % Makes the axis light grey
% Make two rectangular keys out of patches
% Using "UserData" to store what key it is .
% extra arguments throught the 'callback' (e.g., a third input)? Look up
% again
whiteKey1 = patch([0 1 1 0], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'C1');
whiteKey2 = patch([1 2 2 1], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'D1');
whiteKey3 = patch([2 3 3 2], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'E1');
whiteKey4 = patch([3 4 4 3], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'F1');
whiteKey5 = patch([4 5 5 4], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'G1');
whiteKey6 = patch([5 6 6 5], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'A1');
whiteKey7 = patch([6 7 7 6], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'H1');
whiteKey8 = patch([7 8 8 7], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'C2');
whiteKey9 = patch([8 9 9 8], [1 1 7 7], [1 1 1], 'EdgeColor', [0 0 0], 'UserData', 'D2');
%111is white,000is black
blackKey1 = patch([0.6 1.4 1.4 0.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'C#1');
blackKey2 = patch([1.6 2.4 2.4 1.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'D#1');
blackKey3 = patch([3.6 4.4 4.4 3.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'F#1');
blackKey4 = patch([4.6 5.4 5.4 4.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'G#1');
blackKey5 = patch([5.6 6.4 6.4 5.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'A#1');
blackKey6 = patch([7.6 8.4 8.4 7.6], [3.5 3.5 7 7], [0.1 0.1 0.1], 'EdgeColor', [0 0 0], 'UserData', 'C#2');
%same here, putting it in seperate names to avoid overwriting
% creates letters
text(0.4,1.5,'C')
text(1.4,1.5,'D')
text(2.4,1.5,'E')
text(3.4,1.5,'F')
text(4.4,1.5,'G')
text(5.4,1.5,'A')
text(6.4,1.5,'H')
text(7.4,1.5,'C')
text(8.4,1.5,'D')
%creates smaller, white letters on black keys
text(0.85,4,'C#','Color','white','FontSize',9)
text(1.85,4,'D#','Color','white','FontSize',9)
text(3.85,4,'F#','Color','white','FontSize',9)
text(4.85,4,'G#','Color','white','FontSize',9)
text(5.85,4,'A#','Color','white','FontSize',9)
text(7.85,4,'C#','Color','white','FontSize',9)
text(1.5,0,'Now click the matching key on the piano')
% Makes the keys respond to mouse click
whiteKey1.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey2.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey3.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey4.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey5.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey6.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey7.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey8.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
whiteKey9.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey1.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey2.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey3.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey4.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey5.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
blackKey6.ButtonDownFcn = @(src, event) ClickresponseCallback(src, event);
% makes sure the X and Y axis have the same step size; not sure if needed
% seen somewhere
ax.DataAspectRatio = [1 1 1];
% Setup the close request callback so the file can be closed correctly
fh.CloseRequestFcn = @(src, event) closeKeyboard(src, event);
function ClickresponseCallback(src, event)
% exp_directory = "/Users/alinapanzel/Documents/MATLAB/result.txt";
% fileID = fopen(exp_directory'w');
% callback function, but switching behavior based on the source of the callback
%Mathworks
switch class(src)
case {'matlab.graphics.primitive.Patch'}
%this is from a mathworks community member
% Patch clicked, so let's respond with the key value we
% stored fprintf writes data into a textfile
%first input is a string of what will be displayed in the command window
% %d = decimal notation
% \n = new line \t = tab space \
% second input assigns decimal(%d) in the string of value
%this is from a mathworks community member
fileID = getFileID();
fprintf(fileID, 'Clicked key\n\n');
fprintf(fileID,'%s\n', src.UserData);
type('results.txt');
%fprintf('Clicked key %s\n', src.UserData);
%fprintf(fileID 'Clicked key %s\n', src.UserData);
%case {'matlab.graphics.axis.Axes'}
% Axis clicked, so let's respond with the (X, Y) position
% that was clicked
%this is from a mathworks community member
%fprintf('Clicked axis at point %0.3f, %0.3f\n', src.CurrentPoint(1, 1), src.CurrentPoint(1, 2));
%otherwise
%fprintf('Unknown object of type ''%s'' clicked\n', class(src));
end
end
function closeKeyboard(src, event)
% Close the file when the figure is closed so we save the data
try
fileID = getFileID();
fclose(fileID);
delete(src); % Delete the source figure once close operations have finished
catch err
% If anything goes wrong, report it, and delete the figure
warning('Failed to close out properly!')
disp(err);
delete(src)
end
end
function outFID = getFileID()
% This is a helper function to pass around the file ID through the
% script. Using this instead of globals because globals tend to be
% messy. In a more robust system, the whole keyboard should
% probably be made as a class, but for a simple demo, this will do
% just fine
fullFilePath = 'results.txt';
% Make the file identifier persistent so we don't lose it
persistent fid;
% One caveat to persistent variables is that they stick around
% until 'clear all' is used. If this script is run multiple times
% in a row without a 'clear all', the file ID will be leftover from
% a previous run and will now be invalid because 'fclose' was
% called, but it will not be empty. To test for this, we try to
% print an empty string to the file. If there is an error returned
% that matches an invalid file identifier, then it's time to open
% up a new file
doOpenFlag = false;
if isempty(fid)
doOpenFlag = true;
else
try
fprintf(fid, '');
catch err
if strcmp(err.identifier, 'MATLAB:badfid_mx')
% If we can't print to the file because it's invalid,
% that means that the persistent variable is leftover
% from a previous sessions that has ended. Go ahead and
% get rid of it and open a new file
doOpenFlag = true;
else
disp(err);
error('File ID is invalid for an unkown reason');
end
end
end
if doOpenFlag
% Only open the file if the identifier is empty
fprintf('Opening file ''%s''\n', fullFilePath);
fid = fopen(fullFilePath, 'w+');
end
% Cannot return persistent variables directly from a function, so
% pass it out through a dummy variable
outFID = fid;
end

Sign in to comment.

Sign in to answer this question.