# 4. Playback

There are two ways to play haptic effects: **vh.play()** and **vh.queue()**.

- **vh.play()** — **Blocking.** Plays the effect immediately and blocks the code until the effect finishes.
- **vh.queue()** — **Non-blocking.** Adds the effect to a per-channel queue and returns immediately. You must call **vh.renderQueue()** to actually play the queued effects.

Choose `play()` when you need guaranteed continuous output and can afford to block. Choose `queue()` when other tasks (LED updates, sensors, UI) must run in the same loop.

---

## 4.1 Play (Blocking)

**Syntax:** `vh.play({ List of Primitives }, channelSpec);`  
`channelSpec` is optional—channel number, tag expression, or channel vector.

`vh.play()` starts the effect immediately and does not return until the effect has finished playing. Any code after the call is blocked until then.

**Example — Button press with LED (blocking behavior):**

```cpp
#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

const int LedPin = 14;
const int buttonPin = 21;
unsigned long lastBlinkTime = 0;
bool LedState = true;

void setup() {
    Serial.begin(115200);
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});
    pinMode(LedPin, OUTPUT);
    pinMode(buttonPin, INPUT_PULLDOWN);
}

void loop() {
    // LED blinks every 200ms
    if (millis() - lastBlinkTime > 200) {
        LedState = !LedState;
        digitalWrite(LedPin, LedState);
        lastBlinkTime = millis();
    }

    // When button is pressed, haptic plays and blocks — LED stops blinking during effect
    if (digitalRead(BUTTON_PIN)) {
        vh.play({VIBRATE(1000, 0.3, 100.0, 0.0)});  // 1 second vibrate at 100Hz
    }
}
```

When the button is pressed, `play()` blocks for the full duration of the vibrate (1 second). The LED does not blink during that time because the main loop does not advance.

---

## 4.2 Queue and RenderQueue (Non-Blocking)

`vh.queue()` is for cases where other tasks must run in the same loop. It adds effects to a per-channel queue and returns immediately. The effects are **not** played until you call `vh.renderQueue()`.

**Syntax:**
- `vh.queue({ List of Primitives }, channelSpec, operType);` — add effects to the queue
- `vh.renderQueue(channelNo);` — advance playback by one render step

`channelSpec` and `operType` are optional. Use `APPEND_EFFECT` to add to the end or `REPLACE_EFFECT` to clear and replace.

**Example — Button triggers queued effect:**

```cpp
#include <VectorHaptics.h>
#include "YourBoardProfile.h"

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

const int buttonPin = 21;

void setup() {
    Serial.begin(115200);
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});
    pinMode(buttonPin, INPUT_PULLDOWN);
}

void loop() {
    if (digitalRead(BUTTON_PIN)) {
        vh.queue({VIBRATE(1000, 0.3, 100.0, 0.0)}, 1, APPEND_EFFECT);
    }
    vh.renderQueue(1);  // Renders the next frame from the queue
}
```

Each time through the loop, `renderQueue(1)` advances playback by one render step. The main loop keeps running, so you can handle other inputs and tasks.

---

## 4.3 How to Use renderQueue()

`vh.renderQueue(channelNo)` renders a **single frame** of the queued effect each time it is called. It uses a global timer to know where the effect is in time. The **resolution** of the haptic output depends on **how often** you call `renderQueue()`.

### Loop speed affects output quality

If you call `renderQueue()` only occasionally (e.g. every 10ms because of a slow loop), the output will be coarse. The intended waveform (e.g. a sine wave for a vibrate) will be sampled at discrete points. If the loop is too slow, those points can be far apart and the result will look almost flat instead of smooth.

**Example — Slow loop (problematic):**

```cpp
void setup() {
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});
    vh.queue({VIBRATE(5000, 0.3, 100.0, 0.0)}, 1, APPEND_EFFECT);  // 5 second vibrate at 100Hz
}

void loop() {
    vh.renderQueue(1);
    delay(10);  // Simulates a slow loop — rest of your code takes 10ms
}
```

- The **target** haptic output for a 100Hz vibrate is a smooth sine wave:

![Target sine wave for 100Hz vibrate](images/image4.png)

- `renderQueue()` is called every 10ms. It samples the waveform at those discrete times (shown in blue below). The global timer advances between calls:

![Sampled points when renderQueue() is called every 10ms](images/image2.png)

- When the loop is very slow, the effective sample rate is low. The **result** can collapse to a nearly flat line, far from the intended sine wave:

![Flat output when loop is too slow](images/image3.png)

### Fix: Increase loop speed

Call `renderQueue()` more often—either by speeding up the loop or calling it multiple times per loop:

```cpp
void loop() {
    vh.renderQueue(1);
    delay(1);  // Faster loop — output improves
}
```

With a faster loop, the output approaches the target waveform:

![Improved output with faster loop](images/image5.png)

If you need **perfect continuous output** with no sampling artifacts, use **vh.play()** instead of `queue()` + `renderQueue()`. `play()` runs the effect in a dedicated path with high temporal resolution.

---

## 4.4 Using queue() and Channels for Simultaneous Effects

When you pass a channel (or channel list) to `vh.queue()`, the effect is queued on that channel only. If you omit the channel, it is queued on all channels. You can queue different effects on different channels and play them at the same time by calling `renderQueue()` for each channel in the same loop.

**Example — Different effects on different channels:**

```cpp
void setup() {
    channels.add(1, {PIN_LEFT}, {"left"});
    channels.add(2, {PIN_RIGHT}, {"right"});
    vh.init(&board, {&channels});

    // Queue vibrate on channel 1
    vh.queue({VIBRATE(5000, 0.3, 100.0, 0.0)}, 1, REPLACE_EFFECT);

    // Queue a sequence of pulses on channel 2
    vh.queue({
        PULSE(15, 1.0, 1.0),
        PAUSE(1000),
        PULSE(15, 1.0, 1.0),
        PAUSE(1000),
        PULSE(15, 1.0, 1.0)
    }, 2, REPLACE_EFFECT);
}

void loop() {
    vh.renderQueue(1);  // Advance channel 1
    vh.renderQueue(2);  // Advance channel 2
}
```

Both channels advance in parallel each time through the loop, so the vibrate on channel 1 and the pulse sequence on channel 2 play simultaneously.

---

## 4.5 Using Play and Queue Together

You can combine `play()` and `queue()`. `vh.play()` plays an effect **immediately** and can interrupt or override what is currently playing from the queue. Use `play()` for urgent feedback (e.g. emergency or critical events) and `queue()` for normal UI feedback.

**Example — Normal UI feedback + emergency interrupt:**

```cpp
#define BUTTON_PIN      YOUR_BUTTON_GPIO    // replace with your pin
#define EMERGENCY_PIN   YOUR_EMERGENCY_GPIO // replace with your pin

void setup() {
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels});
    pinMode(BUTTON_PIN, INPUT_PULLDOWN);
    pinMode(EMERGENCY_PIN, INPUT_PULLDOWN);
}

void loop() {
    vh.renderQueue(1);  // Render queued effects (non-blocking)

    // Normal UI feedback — add to queue
    if (digitalRead(BUTTON_PIN)) {
        vh.queue({VIBRATE(1000, 0.3, 100.0, 0.0)}, 1, APPEND_EFFECT);
    }

    // Emergency or interrupt — play immediately, overrides queue
    if (digitalRead(EMERGENCY_PIN)) {
        vh.play({TICK(15, 1.0, 1.0)});  // Plays now, interrupts queue
    }
}
```

---

## 4.6 Stop Playback

```cpp
vh.stop();     // Stop all playback immediately
vh.stop(50);   // Ramp down over 50ms
```

---

## Summary: play() vs queue() vs renderQueue()

| Function | Behavior | When to use |
|----------|----------|-------------|
| **vh.play()** | Blocking; plays immediately until effect completes | Guaranteed smooth output; emergency/interrupt feedback |
| **vh.queue()** | Non-blocking; adds effect to channel queue | Other tasks run in the same loop; normal UI feedback |
| **vh.renderQueue()** | Renders one frame from the queue per call | Call repeatedly in a loop; output resolution depends on call frequency |
