10 – “Story Teller” – Prototype Video Demo

Introduction

For the final blog post of this series, I have taken the time to prepare a comprehensive demonstration video, which provides a detailed overview of my low-fidelity prototype. This video is designed to give viewers an understanding of the basic functionalities and design elements of our prototype, showcasing its potential and the direction we are heading in with this project.

Video

Conclusions

The process of prototyping has been incredibly valuable in visualizing the potential of Story Teller. It has allowed me to identify key functionalities and design elements and brought to light areas that need further development and refinement. As I move forward, I plan to conduct further user testing to gain valuable feedback and insights. This feedback will inform my next iterations and help me continually improve and refine the tool. My ultimate goal is to create a user-friendly and intuitive platform that supports the creative process of interactive storytelling, as part of my final Master’s Thesis.

Calm Technology // 20

End of the semester and end of my prototyping journey with Tap in the field of Calm Technology for now. After finishing a polished interface to control Tap, I created an explanatory video about the process of prototyping on Tap and the final working features, which you can watch below. Overall it was a fun experience to dive into this prototyping process and learn a thousand little things along the way, from running stepper motors to writing code for Arduino and that failure is part of the process. Now that it is all done, I would like to thank ChatGPT for helping me, a non-programmer, to code and DeepLWrite for turning my quickly scribbled blog post scripts into easily readable blog posts. Making this whole project possible.

09 – “Story Teller” – Prototype Development

Introduction

After conducting a thorough analysis of different research papers and exploring various existing authoring tools, I embarked on the journey of developing my own prototype. This process involved the examination of the strengths and weaknesses of each tool, as well as an in-depth understanding of the theories and methodologies presented in each research paper. With this knowledge, I then proceeded to apply what I had learned to the creation of my prototype. The objective of this blogpost is to document the process that I underwent during the prototyping phase.

Methodology

I began by listing all the sections I wanted to reimplement from StoryTec, including the G-Flash card system and Story Curve Visualization mode.

  • Story Editor: This is a free canvas where users can drag, resize, move, and zoom in and out of elements.
    • Scenes: These contain characters, interactive elements, and behaviors such as actions, dialogue, and events.
    • Complex Scenes (which I’ll refer to as Stages): These are the environments where scenes take place. They contain stage-specific elements and, of course, the scenes themselves.
  • Stage Editor: In StoryTec, this was a pluggable framework capable of constructing scenes in various ways. For ease of use, I will integrate this as a function of the Story Editor, utilizing a toolbar with drag-and-drop flashcards, similar to G-Flash. In my version, the cards will be fully customizable through the Property Editor.
  • Property Editor: This is a separate tab that adapts based on the selected item. It can add any kind of property to any element.
  • Action Set Editor: In StoryTec, this was a separate editor for setting the logic between element interactions. In my prototype, it will become a button in the Tool tab, connecting elements similarly to how Figma handles prototyping.
  • Asset Manager: A separate tab where uploaded assets can be dragged and dropped either onto the open canvas as images, videos, or sound, or onto elements as properties.
  • Story Curve Visualization: This is an option in the top bar that changes how the scenes are displayed, toggling from the free canvas to a double-axis view of the Story Curve.

After identifying the core features and buttons for my tool, I sketched a basic information architecture on paper. Following this, I created preliminary wireframes to visualize the page’s layout.

Pages, tabs and cards

Starting from the sketches, I prototyped a low-fidelity version of the main pages for my authoring tool.

The home page displays all the stories that a user might want to create, presented in a straightforward gallery.

Each Story file features an „infinite“ zoom canvas where elements, cards, and assets can be dragged and dropped, resized, opened, and minimized. This design maximizes flexibility of thought.

When an element is clicked, the Properties tab displays all details of that specific instance, and allows the creation of new properties if necessary. These properties can be used and referenced as logic variables when establishing connections between elements.

The Stage button in the Bottom Tool Bar offers two main interactions: the New Stage card and the Element card. Both can be dragged onto the canvas and resized to create the desired composition.

Similarly, the Scene button in the Bottom Tool Bar enables the user to draw a new scene or select one of the cards for characters, elements, or behaviors.

Finally, the Story Curve View visualizes different scenes on the Narration order/Story order axis. You can personalize this visualization using the settings tab.

Goals & Conclusion

The main goal of this prototype is to thoroughly test the usability of various complex features, particularly for beginners and new storytellers. I aim to conduct an extensive user test to gain insights into whether the basic principles of User Experience (UX) are being followed and function as intended.

After initial testing and result analysis, I’ll create a higher fidelity prototype. This version will focus more on the user interface, using feedback from the initial tests to ensure a smoother, more intuitive user experience.

In my next blog post, I’ll show the main features of this prototype through a detailed demo video. This will help me show the tool’s functionality and potential.

Calm Technology // 19

Since I had scripted the different gestures I wanted to have for Tap in Arduino, I could now combine all the gestures into one script. I then connected the Wemos board that controls Tap to the wifi so that I could trigger each of these gestures wirelessly from my laptop via a simple interface.

To create the interface, I used Max 8 to build a simple patch to send OSC messages to my Wemos board. The patch consists of a few buttons that trigger messages with a value between 0 & 3, which are then sent as an OSC message via UDP to trigger different parts of the Arduino script.

Max patch interface to control tap

I combined all the different gesture scripts into one Arduino script that can receive OSC messages via UDP. These messages set the value for a variable which is then queried in the loop of the script to start the desired gestures. After the gestures have finished, the variable is reset to put Tap back into a suspended state.

#include <AccelStepper.h>
#include <ESP8266WiFi.h>  // The Library for WIFI
#include <WiFiUdp.h>  // Library for UDP
#include <OSCMessage.h>  //Library for OSC

#define motorInterfaceType 1

// Define the stepper motor and the pins that is connected to // (STEP, DIR)
AccelStepper stepper1(motorInterfaceType, D5, D6); 
AccelStepper stepper2(motorInterfaceType, D7, D8);

// Set the target positions for both steppers
int PositionDown = 0;
int PositionUp = 0;

// State variable to keep track of movement sequence
int state1 = 0;
int state2 = 0;

// For triggering movements
int mode = 0;

WiFiUDP Udp;

const char* ssid = "************";           
const char* password = "************";      

const IPAddress dstIp(192,168,1,129); 
const unsigned int dstPort = 7250;  // Destination OSC
const unsigned int localPort = 7300; // Reciving OSC


////////////////////////////////////////////////////////////////////////////////////


void setup() {
  
  Serial.begin(9600);

  // Settings for Motor 1
  stepper1.setMaxSpeed(1000); 
  stepper1.setAcceleration(500);
  stepper1.setCurrentPosition(0);

  // Settings for Motor 2
  stepper2.setMaxSpeed(1000);
  stepper2.setAcceleration(500);
  stepper2.setCurrentPosition(0);

  // Connecting to WIFI
  Serial.print("Connecting WiFi ");
  // Prevent need for powercyle after upload.
  WiFi.disconnect();

  // Use DHCP to connect and obtain IP Address.
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid,password);

  // Wait until we have connected to the WiFi AP.
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("Done!");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  Udp.begin(localPort);

}


////////////////////////////////////////////////////////////////////////////////////


void loop() {

  handleOSC();

  if (mode == 0) {

    state1 = 0;
    state2 = 0;

  }

  if (mode == 1) {

    Wave1();
    Wave2();

  }

  if (mode == 2) {
    
    Knock1();

  }

  if (mode == 3) {
    
    Tap1();
    Tap2();

  }

}


////////////////////////////////////////////////////////////////////////////////////


void Wave1() {

  switch (state1) {

    case 0:
      stepper1.setAcceleration(200);
      PositionDown = 40;
      stepper1.moveTo(PositionDown);
      state1 = 1;
      break;
    
    case 1:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = -40;
        stepper1.moveTo(PositionDown);
        state1 = 2;
      }
      break;
    
    case 2:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = 20;
        stepper1.moveTo(PositionDown);
        state1 = 3;
      }
      break;

    case 3:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = -20;
        stepper1.moveTo(PositionDown);
        state1 = 4;
      }
      break;

    case 4:
      stepper1.setAcceleration(100);
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = 0;
        stepper1.moveTo(PositionDown);
        state1 = 5;
      }
      break;
    
    case 5:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
      }
      break;
  }
}


void Wave2() {

  if (state1 >= 4){

    switch (state2) {

      case 0:
        stepper2.setAcceleration(275);
        PositionUp = 25;
        stepper2.moveTo(PositionUp);
        state2 = 1;
        break;
      
      case 1:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = -25;
          stepper2.moveTo(PositionUp);
          state2 = 2;
        }
        break;
      
      case 2:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = 15;
          stepper2.moveTo(PositionUp);
          state2 = 3;
        }
        break;

      case 3:
        
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = -15;
          stepper2.moveTo(PositionUp);
          state2 = 4;
        }
        break;

      case 4:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = 15;
          stepper2.moveTo(PositionUp);
          state2 = 5;
        }
        break;

      case 5:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = -15;
          stepper2.moveTo(PositionUp);
          state2 = 6;
        }
        break;

      case 6:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = 0;
          stepper2.moveTo(PositionUp);
          state2 = 7;
        }
        break;  
      
      case 7:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          mode = 0;          
        }
        break;
    }
  }
}


void Knock1() {

  switch (state1) {

    case 0:
      stepper2.setAcceleration(400);
      PositionUp = -20;
      stepper2.moveTo(PositionUp);
      state1 = 1;
      break;
    
    case 1:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 80;
        stepper2.moveTo(PositionUp);
        state1 = 2;
      }
      break;
    
    case 2:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 60;
        stepper2.moveTo(PositionUp);
        state1 = 3;
      }
      break;

    case 3:
      stepper2.setAcceleration(800);
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 80;
        stepper2.moveTo(PositionUp);
        state1 = 4;
      }
      break;

    case 4:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 0;
        stepper2.moveTo(PositionUp);
        state1 = 5;
      }
      break;
    
    case 5:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        mode = 0;
      }
      break;
  }
}


void Tap1() {

  if (state2 >= 2){

    switch (state1) {

      case 0:
        stepper1.setAcceleration(300);
        PositionDown = -20;
        stepper1.moveTo(PositionDown);
        state1 = 1;
        break;
        
      case 1:
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
          PositionDown = 5;
          stepper1.moveTo(PositionDown);
          state1 = 2;
        }
        break;
        
      case 2:
        stepper1.setAcceleration(600);
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
          PositionDown = -20;
          stepper1.moveTo(PositionDown);
          state1 = 3;
        }
        break;

      case 3:
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
          PositionDown = 0;
          stepper1.moveTo(PositionDown);
          state1 = 4;
        }
        break;

      case 4:
        stepper1.setAcceleration(100);
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
        }
        break;
      
    }
  }
}


void Tap2() {

  switch (state2) {

    case 0:
      stepper2.setAcceleration(400);
      PositionUp = 50;
      stepper2.moveTo(PositionUp);
      state2 = 1;
      break;
    
    case 1:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 0;
        stepper2.moveTo(PositionUp);
        state2 = 2;       
      }
      break;

    case 2:
      if (state1 >= 4) {
        stepper2.setAcceleration(200);
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          mode = 0;
        }
      }
      break;
        
  }
}


////////////////////////////////////////////////////////////////////////////////////


void handleOSC() {
  
  OSCMessage msg("/Mode");
  int size = Udp.parsePacket();
  if (size > 0) {
    while (size--) {
      msg.fill(Udp.read());
    }
    if (!msg.hasError()) {
      msg.dispatch("/Mode", Activating);
    }
  }
}


void Activating(OSCMessage &msg) {
  
  if (msg.isInt(0)) {
    int receivedMode = msg.getInt(0);
    mode = receivedMode;
    Serial.println("Received Mode Command");
    Serial.println(mode);

  }
}

After finishing the interface and the now complete Arduino script, I tested them together. With my laptop set up as a touch interface running Max 8 and the Wemos board running Tap as usual. All went quite smoothly and after some tweaking everything is now running stable. You can see the result below.

Tap controlled via a remote interface

With this, my prototype for this semester’s Design & Research project is almost finished. The last step will be to make the interface a bit cleaner and more intuitive, and then I will make a video explaining my prototype Tap and how it works.

Visual Prototype

After spending some weeks developing a technical prototype, I started building a visual one as well. The visual prototype represents the size and appearance of my concept, but serves no functionality. It is made of paper, cardboard, plastic, tape and fabric. 

I started drawing some sketches of the intended design. In the blog post Concept Definition and Aim from May, I stated what buttons I find important to include. I worked further with these and concluded to implement seven buttons: 

  • Power on/off
  • Sound up/down
  • Play melody/record
  • Replay melody
  • Return to previous melody
  • Pause melody
  • Skip to next melody

I want the design to be clean and understandable, without any unnecessary buttons. But after my user tests, I also understood that buttons such as replay and skip were crucial in order to serve good user experiences. 

The lower part of the product is the speaker, and the upper part involve the buttons. The biggest button in the middle is the one that will be used the most. When it is pressed, the random generated melody plays. This is also the button that must be pressed in order to record the sound of the user input. In the next version of my prototype, I will also include a LED ring around that gives instant feedback on the input. 

Calm Technology // 18

With my new prototype set up, I was ready to start scripting the different gestures for Tap. I decided to make three different gestures for now: the wave, the tap and the knock. The wave for Tap to say hello or to acknowledge a new command. The tap as a way of interacting with other objects and bringing them into the user’s focus. And finally, the knock as a more analogue way of notifying the user of, for example, a timer or a specific event.

The first gesture is the wave gesture. It is intended to let the user know that Tap is active or has accepted the new command or task. In this movement, the top rotates from left to right and the tapper waves from one side to the other a few times.

Tap waving
#include <AccelStepper.h>

#define motorInterfaceType 1

// Define the stepper motor and the pins that is connected to // (STEP, DIR)
AccelStepper stepper1(motorInterfaceType, D5, D6); 
AccelStepper stepper2(motorInterfaceType, D7, D8);

// Set the target positions for both steppers
int PositionDown = 0;
int PositionUp = 0;

// State variable to keep track of movement sequence
int state1 = 0;
int state2 = 0;

////////////////////////////////////////////////////////////////////////////////////


void setup() {
  
  Serial.begin(9600);

  // Settings for Motor 1
  stepper1.setMaxSpeed(1000); 
  stepper1.setAcceleration(500);
  stepper1.setCurrentPosition(0);

  // Settings for Motor 2
  stepper2.setMaxSpeed(1000);
  stepper2.setAcceleration(500);
  stepper2.setCurrentPosition(0);

}


////////////////////////////////////////////////////////////////////////////////////


void loop() {
  
  Wave1();
  Wave2();

}


////////////////////////////////////////////////////////////////////////////////////


void Wave1() {

  switch (state1) {

    case 0:
      stepper1.setAcceleration(200);
      PositionDown = 40;
      stepper1.moveTo(PositionDown);
      state1 = 1;
      break;
    
    case 1:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = -40;
        stepper1.moveTo(PositionDown);
        state1 = 2;
      }
      break;
    
    case 2:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = 20;
        stepper1.moveTo(PositionDown);
        state1 = 3;
      }
      break;

    case 3:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = -20;
        stepper1.moveTo(PositionDown);
        state1 = 4;
      }
      break;

    case 4:
      stepper1.setAcceleration(100);
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
        PositionDown = 0;
        stepper1.moveTo(PositionDown);
        state1 = 5;
      }
      break;
    
    case 5:
      if (stepper1.distanceToGo() != 0) {
        stepper1.run();
      } else {
        stepper1.stop();
      }
      break;

  }
}


void Wave2() {

  if (state1 >= 4){

    switch (state2) {

      case 0:
        stepper2.setAcceleration(275);
        PositionUp = 25;
        stepper2.moveTo(PositionUp);
        state2 = 1;
        break;
      
      case 1:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = -25;
          stepper2.moveTo(PositionUp);
          state2 = 2;
        }
        break;
      
      case 2:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = 15;
          stepper2.moveTo(PositionUp);
          state2 = 3;
        }
        break;

      case 3:
        
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = -15;
          stepper2.moveTo(PositionUp);
          state2 = 4;
        }
        break;

      case 4:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = 15;
          stepper2.moveTo(PositionUp);
          state2 = 5;
        }
        break;

      case 5:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = -15;
          stepper2.moveTo(PositionUp);
          state2 = 6;
        }
        break;

      case 6:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
          PositionUp = 0;
          stepper2.moveTo(PositionUp);
          state2 = 7;
        }
        break;  
      
      case 7:
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
        }
        break;

    }
  }
}

The second gesture is the tap gesture. It is intended as an object-based reminder for things like drinking water or airing out your room. It can also be used in combination with objects to create a different sound for timers and reminders than knocking on the ground. In this gesture, the tapper moves down a quarter turn and the entire base then rotates the tapper twice against the object before returning to the default position.

Tap tapping
#include <AccelStepper.h>

#define motorInterfaceType 1

// Define the stepper motor and the pins that is connected to // (STEP, DIR)
AccelStepper stepper1(motorInterfaceType, D5, D6); 
AccelStepper stepper2(motorInterfaceType, D7, D8);

// Set the target positions for both steppers
int PositionDown = 0;
int PositionUp = 0;

// State variable to keep track of movement sequence
int state1 = 0;
int state2 = 0;

////////////////////////////////////////////////////////////////////////////////////


void setup() {
  
  Serial.begin(9600);

  // Settings for Motor 1
  stepper1.setMaxSpeed(1000); 
  stepper1.setAcceleration(500);
  stepper1.setCurrentPosition(0);

  // Settings for Motor 2
  stepper2.setMaxSpeed(1000);
  stepper2.setAcceleration(500);
  stepper2.setCurrentPosition(0);

}


////////////////////////////////////////////////////////////////////////////////////


void loop() {
  
  Tap1();
  Tap2();

}


////////////////////////////////////////////////////////////////////////////////////


void Tap1() {

  if (state2 >= 2){

    switch (state1) {

      case 0:
        stepper1.setAcceleration(300);
        PositionDown = -20;
        stepper1.moveTo(PositionDown);
        state1 = 1;
        break;
        
      case 1:
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
          PositionDown = 5;
          stepper1.moveTo(PositionDown);
          state1 = 2;
        }
        break;
        
      case 2:
        stepper1.setAcceleration(600);
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
          PositionDown = -20;
          stepper1.moveTo(PositionDown);
          state1 = 3;
        }
        break;

      case 3:
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
          PositionDown = 0;
          stepper1.moveTo(PositionDown);
          state1 = 4;
        }
        break;

      case 4:
        stepper1.setAcceleration(100);
        if (stepper1.distanceToGo() != 0) {
          stepper1.run();
        } else {
          stepper1.stop();
        }
        break;
      
    }
  }
}


void Tap2() {

  switch (state2) {

    case 0:
      stepper2.setAcceleration(400);
      PositionUp = 50;
      stepper2.moveTo(PositionUp);
      state2 = 1;
      break;
    
    case 1:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 0;
        stepper2.moveTo(PositionUp);
        state2 = 2;       
      }
      break;

    case 2:
      if (state1 >= 4) {
        stepper2.setAcceleration(200);
        if (stepper2.distanceToGo() != 0) {
          stepper2.run();
        } else {
          stepper2.stop();
        }
      }
      break;
        
  }
}

The third gesture is the knock gesture. It is intended as an analogue notification for timers, reminders or other events. In this gesture, only the tapper itself moves. It moves slightly backwards and then knocks twice on the ground before returning to its normal position.

Tap knocking
#include <AccelStepper.h>

#define motorInterfaceType 1

// Define the stepper motor and the pins that is connected to // (STEP, DIR)
AccelStepper stepper1(motorInterfaceType, D5, D6); 
AccelStepper stepper2(motorInterfaceType, D7, D8);

// Set the target positions for both steppers
int PositionDown = 0;
int PositionUp = 0;

// State variable to keep track of movement sequence
int state1 = 0;
int state2 = 0;

////////////////////////////////////////////////////////////////////////////////////


void setup() {
  
  Serial.begin(9600);

  // Settings for Motor 1
  stepper1.setMaxSpeed(1000); 
  stepper1.setAcceleration(500);
  stepper1.setCurrentPosition(0);

  // Settings for Motor 2
  stepper2.setMaxSpeed(1000);
  stepper2.setAcceleration(500);
  stepper2.setCurrentPosition(0);

}


////////////////////////////////////////////////////////////////////////////////////


void loop() {
  
  Knock1();

}


////////////////////////////////////////////////////////////////////////////////////


void Knock1() {

  switch (state1) {

    case 0:
      stepper2.setAcceleration(400);
      PositionUp = -20;
      stepper2.moveTo(PositionUp);
      state1 = 1;
      break;
    
    case 1:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 80;
        stepper2.moveTo(PositionUp);
        state1 = 2;
      }
      break;
    
    case 2:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 60;
        stepper2.moveTo(PositionUp);
        state1 = 3;
      }
      break;

    case 3:
      stepper2.setAcceleration(800);
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 80;
        stepper2.moveTo(PositionUp);
        state1 = 4;
      }
      break;

    case 4:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
        PositionUp = 0;
        stepper2.moveTo(PositionUp);
        state1 = 5;
      }
      break;
    
    case 5:
      if (stepper2.distanceToGo() != 0) {
        stepper2.run();
      } else {
        stepper2.stop();
      }
      break;

  }
}


The scripting and testing of the gestures went almost smoothly with the help of ChatGPT for the coding part. Except for one problem where the lower motor started to overheat and slowly melt my 3D print. Luckily I caught it before it did any irreversible damage. The problem ended up being that I set the current flow in the motor controller for the lower motor too high, which was quickly fixed and everything went smoothly again. From here I will now start to implement all the different gestures into one script and add the functionality of triggering the different gestures via OSC messages so that I can then create a dedicated interface to control Tap.

Calm Technology // 17

After I started programming specific gestures for Tap, the cardboard and hot glue construction began to fail. This meant that the top motor was slowly falling backwards and I couldn’t operate the prototype in this state. So I decided to put the gesture work on hold until the next step and build a more robust prototype for it.

I started by measuring the two motors and getting the dimensions of my paper prototype. I then scribbled a few quick and dirty drawings of some of the possible solutions. When I was happy with my solution on paper, how the construction should look and that the motors, cables and everything had its place, I moved on to 3D modelling. I started by modelling the two motors to have a reference point and then went on to model the base to hold the top motor and a tapper that would fit perfectly on the motor’s axis. For the time being, I decided to build only the upper half of the tap, as that was all that was needed for now.

CAD Model SOLIDWORKS

I then loaded my finished 3D model into my FDM printer and printed the base for the upper motor in neutral white and the tapper in dark blue. Both colours stayed in the scheme of my already built power supply to create a certain consistency. The whole printing process took about 5 1/2 hours and then the parts were ready for assembly.

3D Printing the tapper

Once I had disassembled the cardboard and the hot glue build and had freed up my two motors, I checked to see if the two new parts would fit on their axles. Both fit perfectly and the assembly was quick and easy. Which shows that measuring twice as I did this time has its advantages, unlike when I build the case for my power supply. To finish, I glued the lid of the base and the upper motor inside the base with Pattafix, which is a strong enough adhesive for now and makes dismantling or replacing parts easier.

Old and new prototype parts

With the newly completed version of the prototype, I then did some live testing to see if it would hold up in motion. For the test, I scripted a short moving gesture for Tap, based on the script from the last blog post. As you can see in the video below, everything stayed in place and the setup worked even stabler and smoother than the cardboard version.

Waving Test of Tap

With Tap set up like this, I can now move on to the next steps, as mentioned in my last blog post, to start scripting different gestures and behaviours for Tap. Giving him the ability to communicate by moving his tapper and his body.

Findings from User Tests

I recently conducted user tests on my technical prototype. The prototype does not yet work exactly as I vision my final product, but I find it important to involve users at an early stage. I wanted to test the core idea and get valuable feedback for further development of the concept. 

I chose to conduct the user tests with people I interviewed in my research phase. They are women in the age range 23 to 26 and went to music lessons for 1-3 years as children. They have been playing different instruments such as piano, guitar, drums and the clarinet, but never for a long period of time. To learn more about their musical background, read my blog post from January: Key Findings from Interviews

User test 1

Successful melodies: 4

Observations

  • Wants to test all the buttons before starting the game
  • Tries to sing the melody outload right away after hearing it
  • Points with her fingers on the buttons
  • Uses time to think before trying
  • Improved skills after every try
  • Gets frustrated when failing, but always wants to try again
  • Missing a replay button, to hear the melody again without needing to play it

Playing when looking at the serial monitor (printed notes): much easier, higher level of success. Wants to retry every time she fails. 

General feedback

  • Thought it was very fun to play
  • Felt competitive, did not want to quit
  • Found it very annoying to fail
  • In the beginning, she did not understand that every melody was not necessarily containing all the tones.
  • Low quality speaker makes it hard to separate the tones. Suggests using Max 8 instead of the piezo buzzer. 
  • The “incorrect melody”-sound is similar to some of the melody tones. This could be distracting and confusing. 
  • After successfully playing a melody, the next one plays right away. This was too fast for her to prepare for listening again.
  • If I want to upgrade the product and make it even harder, it could be an idea to also implement different rhythms in the melody. Another option is to make the melodies longer (more than four tones). Nevertheless, she states that it was already complicated enough for her. 

User test 2

Successful melodies: 1

Observations

  • Wants to start right away, before I am finished with the explanation
  • States that she is terrible, does not know anything about music theory
  • Struggles, but learns quickly
  • Notices that she is playing the melody wrong, but struggles to point out what the problem is
  • Decides to give up after numerous attempts on the second melody

Playing when looking at the serial monitor (printed notes): easier, but still needs to think a lot and use multiple attempts to succeed. 

General feedback

  • Said it was very fun, would love to play with it all the time as a child. 
  • Very nice way to train your ear. 
  • Thought it would be easier with a higher quality speaker. 
  • Suggested implementing an orange light for better feedback. It would make it easier to understand the number of wrong notes.  
  • Wanted to try again every time she failed, but it was easy to get stuck on one melody and become annoyed. 
  • Hard to hear what is wrong. 
  • Missed a replay button. 
  • Suggested writing the name of the tones on the buttons. Thinks it would be easier to understand the connections. 
  • Suggested removing the resistors to get brighter LED lights.

Conclusion

In general, it was very helpful to test my concept in such an early stage. Their positive feedback motives me to develop the idea further in the future, and their critique made it clear what changes I should make. I already did some adjustments: 

  • Changed the pitch on some of the tones
  • Increased delay between feedback and new melodies
  • Removed resistors from the breadboard
  • Adjusted the printed messages to the serial monitor