# 5. Audio to Haptics

VectorHaptics supports two ways to turn audio into haptics: **PCM primitive** (pre-recorded waveforms) and **audio streaming** (live audio from A2DP Bluetooth, I2S, or other sources). This guide walks through each approach.

---

## 5.1 PCM Primitive

The PCM primitive plays raw 8-bit unsigned waveform data. You supply a buffer of samples; VectorHaptics plays them at a configurable sample rate. Use this when you have pre-designed haptic waveforms (e.g. from a design tool) or want to sequence custom shapes.

### Sample format

Samples are 8-bit unsigned: **127** = silence (neutral position), **0** = minimum deflection one way, **255** = maximum deflection the other. The default sample rate is 8000 Hz; set it with `VH_Pcm::setSampleRate(8000)` if your waveform was designed for a different rate.

### Basic PCM

Define your waveform as an array, then play it with `PCM(data, size)`:

```cpp
const uint8_t myPcmData[] = {
    127, 140, 153, 166, 179, 191, 204, 217, 230, 242, 255,
    242, 230, 217, 204, 191, 179, 166, 153, 140, 127,
    114, 101,  88,  75,  63,  50,  37,  25,  12,   0,
     12,  25,  37,  50,  63,  75,  88, 101, 114, 127
};

vh.play(PCM(myPcmData, sizeof(myPcmData)));
```

The array holds the waveform samples. `PCM(myPcmData, sizeof(myPcmData))` creates a PCM effect from the buffer—the macro takes the data pointer and the sample count. `vh.play()` plays it and blocks until playback finishes.

### PCM in a sequence

You can combine PCM with other primitives. Each effect in the list plays in order:

```cpp
vh.play({
    PULSE(30, 1.0, 0.8),
    PAUSE(50),
    PCM(myPcmData, sizeof(myPcmData)),
    PAUSE(50),
    TICK(30, 0.9, 1.0)
});
```

Here, a short pulse leads in, then a pause, then your custom waveform, another pause, and finally a sharp tick. The whole sequence runs as one effect and blocks until done.

### PCM sample rate

If your waveform was designed for a different sample rate, set it before playing:

```cpp
VH_Pcm::setSampleRate(16000);  // 16 kHz for finer temporal resolution
vh.play(PCM(myPcmData, sizeof(myPcmData)));
```

---

## 5.2 Audio Streaming (Live Audio)

Audio streaming converts **live audio** (e.g. from A2DP Bluetooth, I2S input, or a microphone) into haptic output in real time. Your board feeds audio samples into a ring buffer; VectorHaptics reads from it and drives the actuators. Used for music-to-haptics, game audio feedback, and similar applications.

### The ring buffer and AudioStreaming

You need two things: a **ring buffer** to hold incoming 16-bit samples, and an **AudioStreaming** object that reads from it and feeds the haptic pipeline. The ring buffer must be statically allocated—no `malloc` in real-time paths. Your board’s audio callback writes into it; the engine reads.

First, allocate the storage and create the buffer:

```cpp
int16_t ringBufStorage[4096];
VH::RingBuffer<int16_t> ringBuffer(ringBufStorage, 4096);
```

`ringBufStorage` is the raw array. `ringBuffer` wraps it as a lock-free single-producer single-consumer ring buffer—your callback is the producer, the engine is the consumer.

Then create the streaming engine. The parameters must match your audio source:

```cpp
AudioStreaming audioStream(44100, 2, 16);  // 44.1 kHz, stereo, 16-bit
```

### Initialization

In `setup()`, add your haptic channel, initialize VectorHaptics with both the channels and the audio streaming module, and bind the ring buffer to the stream:

```cpp
void setup() {
    Serial.begin(115200);
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});

    vh.init(&board, {&channels, &audioStream});
    audioStream.init(&ringBuffer);

    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 1.0f), 0);
}
```

`vh.init()` registers both the channel manager and the audio streaming module. `audioStream.init(&ringBuffer)` connects the ring buffer to the stream—single-buffer mode means mono or mixed L+R. `AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 1.0f)` starts an infinite stream at full intensity. The `0` in `vh.play(..., 0)` targets all channels.

### The audio callback

Your board’s audio driver (A2DP callback, I2S read, etc.) must call a function when samples arrive. That function writes them into the ring buffer:

```cpp
void onAudioDataReceived(const int16_t* samples, size_t count) {
    audioStream.write(samples, count);
}
```

`count` is the total number of 16-bit samples (e.g. frames × channels for interleaved stereo). `audioStream.write()` pushes them into the ring buffer. The engine reads from the other end and converts them to haptic output. Your `loop()` typically does nothing for audio—processing happens in a timer ISR.

### Complete basic example

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

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

int16_t ringBufStorage[4096];
VH::RingBuffer<int16_t> ringBuffer(ringBufStorage, 4096);
AudioStreaming audioStream(44100, 2, 16);

void setup() {
    Serial.begin(115200);
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels, &audioStream});
    audioStream.init(&ringBuffer);
    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 1.0f), 0);
}

void onAudioDataReceived(const int16_t* samples, size_t count) {
    audioStream.write(samples, count);
}

void loop() {
    // Audio is processed via timer ISR — nothing to do here
    // Your board-specific code calls onAudioDataReceived() when data arrives
}
```

---

## 5.3 Audio Streaming Multi-Channel

For stereo or multi-channel input, use **separate ring buffers per audio channel**. Each buffer holds one channel (left, right, etc.). You can route each channel to different haptic actuators.

### Stereo setup

Create two buffers, one for left and one for right:

```cpp
int16_t ringBufLStorage[4096];
int16_t ringBufRStorage[4096];
VH::RingBuffer<int16_t> ringBufL(ringBufLStorage, 4096);
VH::RingBuffer<int16_t> ringBufR(ringBufRStorage, 4096);
VH::RingBuffer<int16_t>* stereoBuffers[2] = { &ringBufL, &ringBufR };
```

`stereoBuffers` is an array of buffer pointers—index 0 = left, 1 = right. The engine de-interleaves incoming stereo and writes each channel into its buffer.

Initialize the stream with the per-channel buffers. The second argument must match the input channel count:

```cpp
AudioStreaming audioStream(44100, 2, 16);
audioStream.init(stereoBuffers, 2);
```

### Routing audio to haptic channels

Use `AUDIO_STREAM`’s third parameter to choose which audio channel drives the output. `0` = mix all channels; `1` = left only; `2` = right only:

```cpp
vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 0), 0);  // Mix all → all haptic channels
vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 1), 1);  // Left audio only → channel 1
vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 2), 2);  // Right audio only → channel 2
```

### The callback for stereo

For interleaved stereo `[L0, R0, L1, R1, ...]`, each frame is two samples. The callback receives stereo frames, so total samples = `frameCount * 2`:

```cpp
void onAudioDataReceived(const int16_t* samples, size_t frameCount) {
    audioStream.write(samples, frameCount * 2);
}
```

### Complete stereo example

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

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

int16_t ringBufLStorage[4096];
int16_t ringBufRStorage[4096];
VH::RingBuffer<int16_t> ringBufL(ringBufLStorage, 4096);
VH::RingBuffer<int16_t> ringBufR(ringBufRStorage, 4096);
VH::RingBuffer<int16_t>* stereoBuffers[2] = { &ringBufL, &ringBufR };
AudioStreaming audioStream(44100, 2, 16);

void setup() {
    Serial.begin(115200);
    channels.add(1, {PIN_LEFT}, {"left", "motor"});
    channels.add(2, {PIN_RIGHT}, {"right", "motor"});
    vh.init(&board, {&channels, &audioStream});
    audioStream.init(stereoBuffers, 2);

    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f, 0), 0);
}

void onAudioDataReceived(const int16_t* samples, size_t frameCount) {
    audioStream.write(samples, frameCount * 2);
}

void loop() {}
```

---

## 5.4 Mixing Primitives with Streamed Haptics

You can overlay haptic primitives (PULSE, VIBRATE, TICK, etc.) on top of a running audio stream. The mixer combines both signals before output.

### Enabling mixing

Call `enableMixing()` on the channel before starting the stream. `ADDITIVE_MIX` sums the audio and primitive samples and clamps the result:

```cpp
audioStream.enableMixing(0, MixingStrategy::ADDITIVE_MIX);
vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f), 0);
```

### Overlaying primitives

Once the stream is running, play primitives as usual. They are mixed with the stream:

```cpp
vh.play(PULSE(50, 1.0, 0.8), 0);
vh.play(VIBRATE(300, 0.5, 160.0, 0.3), 0);
```

Both the stream and the primitives contribute to the output. Other mixing strategies: `INTERRUPT_MIX` (primitive overrides audio), `LARGESTVAL_MIX` (output = max of the two), `CUSTOM_MIX` (custom callback).

### Complete mixing example

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

VectorHaptics vh;
YourBoardProfile board;
VHChannels channels;

int16_t ringBufStorage[4096];
VH::RingBuffer<int16_t> ringBuffer(ringBufStorage, 4096);
AudioStreaming audioStream(44100, 2, 16);

void setup() {
    Serial.begin(115200);
    channels.add(1, {YOUR_OUTPUT_PIN}, {"main"});
    vh.init(&board, {&channels, &audioStream});
    audioStream.init(&ringBuffer);
    audioStream.enableMixing(0, MixingStrategy::ADDITIVE_MIX);
    vh.play(AUDIO_STREAM(VH_AUDIO_STREAM_INFINITY, 0.8f), 0);
}

void onAudioDataReceived(const int16_t* samples, size_t count) {
    audioStream.write(samples, count);
}

void loop() {
    vh.play(PULSE(50, 1.0, 0.8), 0);
    delay(2000);
    vh.play(VIBRATE(300, 0.5, 160.0, 0.3), 0);
    delay(2000);
}
```

See [CODE_EXAMPLES.md § Audio Streaming](../CODE_EXAMPLES.md) for more examples.
