Metadata-Version: 2.4
Name: potatoslicer-client
Version: 0.1.0
Summary: Potatoslicer — chiptune audio engine with tracker-style sequencer
Author: Potatoslicer
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Topic :: Multimedia :: Sound/Audio :: Sound Synthesis
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: numpy
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"

# Potatoslicer Sequencer — Audio Client

Chiptune-style audio engine and player. No audio files — all sound is generated procedurally from JSON song data.

## Architecture

```
SoundSystem (facade)
  → PotatoslicerEngine
    → Sequencer (tracker-style pattern playback)
    → Synth (voice pool + instruments)
      → Voice (oscillator + envelope per note)
    → Mixer (sum + soft clip)
  → AudioDevice (sounddevice callback → speakers)
```

### Voice Model

- Fixed pool of 20 voices
- Each `note_on` allocates a voice from the pool
- Chords allocate one voice per note in the chord
- Detune allocates an additional voice per note (±pitch offset)
- Voice stealing: inactive → released → oldest active
- A channel can own multiple voices (for chords)
- `note_off` / `---` releases all voices on that channel

### Signal Path

```
Oscillator (pulse/triangle/saw/noise)
  → ADSR Envelope
  → Voice output
    → Mixer (sum all active voices → tanh soft clip → master volume)
      → sounddevice callback → speakers
```

## Quick Start

```python
from potatoslicer.sound_system import SoundSystem

sound = SoundSystem()
sound.start()
sound.play_song('groove')  # loads from songs/groove.json
sound.play_sfx('confirm')           # trigger a preset sound effect
sound.stop()
```

### Keyboard Controls (in installer)

- `M` — toggle audio on/off
- Audio state shown in top-right status bar

## Song Format

Songs are JSON files in `potatoslicer/songs/`.

### Example

```json
{
  "title": "My Song",
  "tempo": 122,
  "rows_per_beat": 4,
  "order": [0, 0, 1, 1, 2, 3],
  "instruments": {
    "stab": {
      "waveform": "pulse",
      "duty_cycle": 0.25,
      "attack": 0.001,
      "decay": 0.04,
      "sustain": 0.0,
      "release": 0.03,
      "volume": 0.13,
      "detune": 0.003
    },
    "bass": {
      "waveform": "triangle",
      "attack": 0.001,
      "decay": 0.05,
      "sustain": 0.5,
      "release": 0.04,
      "volume": 0.52
    }
  },
  "patterns": {
    "0": [
      [
        {"channel": 0, "note": "C-1", "instrument": "kick"},
        {"channel": 4, "note": ["D-4", "F-4", "A-4"], "instrument": "stab"}
      ],
      [],
      [{"channel": 2, "note": "C-5", "instrument": "hat"}],
      []
    ]
  }
}
```

### Top-Level Fields

| Field | Description |
|-------|-------------|
| `title` | Song name (display only) |
| `tempo` | BPM (beats per minute) |
| `rows_per_beat` | Rows per beat. 4 = 16th note resolution |
| `order` | Pattern play sequence, loops when finished |
| `instruments` | Named instrument definitions |
| `patterns` | Numbered patterns, each a list of rows |

### Instrument Fields

| Field | Default | Description |
|-------|---------|-------------|
| `waveform` | `pulse` | `pulse`, `square`, `triangle`, `saw`, `noise` |
| `duty_cycle` | `0.5` | Pulse width (0.0–1.0). Only affects pulse waveform |
| `attack` | `0.01` | Seconds to reach full volume |
| `decay` | `0.05` | Seconds to drop from full to sustain |
| `sustain` | `0.6` | Hold level (0.0–1.0) |
| `release` | `0.1` | Seconds to fade after note off |
| `volume` | `0.7` | Output volume (0.0–1.0) |
| `vibrato_depth` | `0.0` | Pitch modulation depth (0.002 = subtle) |
| `vibrato_rate` | `5.0` | Vibrato speed in Hz |
| `detune` | `0.0` | Spawns detuned voice pair for thickness. 0.003–0.005 = subtle widening |

### Pattern Events

Each row is a list of events (or empty `[]` for silence):

```json
{"channel": 0, "note": "C-5", "instrument": "lead", "volume": 0.8}
```

| Field | Description |
|-------|-------------|
| `channel` | Voice channel (0–6 music, 7 reserved for SFX) |
| `note` | Single note `"C-5"`, chord `["C-4","E-4","G-4"]`, or `"---"` for note off |
| `instrument` | Instrument name |
| `volume` | Optional, 0.0–1.0 |

### Chords

`note` can be a single string or an array of note strings:

```json
{"channel": 4, "note": ["D-4", "F-4", "A-4"], "instrument": "stab"}
```

This triggers all notes simultaneously on the same channel. `"---"` releases all voices on the channel.

**Voice cost:** Each note in a chord uses one voice. With `detune > 0`, each note uses two voices. A 3-note chord with detune = 6 voices.

### Note Format

- `C-5` — C in octave 5
- `C#4` — C sharp in octave 4
- `D#3` — D sharp in octave 3
- `---` — note off (releases all voices on channel)

### Timing

```
seconds_per_row = 60 / tempo / rows_per_beat
```

| Tempo | Rows/beat | Row duration |
|-------|-----------|-------------|
| 120 | 4 | 125ms |
| 122 | 4 | 123ms |
| 140 | 4 | 107ms |

## Current Song: `groove.json`

**"Chorus Lift"** at 122 BPM.

Structure: `[0, 0, 1, 1, 2, 3, 1, 0]` — 8 patterns, ~32 bars

| Pattern | Character | Description |
|---------|-----------|-------------|
| 0 | Intro/groove | Kick + bass + hats, stab chord enters halfway |
| 1 | Main | Full groove with clap, stab chords (Dm, Bb) |
| 2 | Breakdown | Hats only + single stab hit |
| 3 | Build | Accelerating kicks, claps, stab chords |

Instruments: kick (triangle), hat (noise), clap (noise), bass (triangle), stab (detuned pulse chords)

## SFX Presets

Defined in `presets.py`:

| Name | Sound | Use |
|------|-------|-----|
| `confirm` | High bleep | Button press, selection |
| `cancel` | Low tone | Back, cancel |
| `error` | Noise burst | Error state |
| `click` | Short tick | UI interaction |
| `deploy_start` | Ascending tone | Deploy begins |
| `deploy_done` | Sustained tone | Deploy success |
| `deploy_fail` | Descending buzz | Deploy failure |

## Song Tool

CLI for editing songs.

```bash
cd potatoslicer-client-python

# Show song metadata
python -m potatoslicer.song_tool info groove

# Export to editable tracker text format
python -m potatoslicer.song_tool export groove
# Creates: groove_edit.txt

# Edit the file, then reimport
python -m potatoslicer.song_tool import groove_edit.txt

# Preview (plays for 10s, Ctrl-C to stop)
python -m potatoslicer.song_tool play groove

# Change tempo
python -m potatoslicer.song_tool tempo groove 130
```

### Tracker Text Format

The export format is human-readable and editable:

```
TITLE: Chorus Lift
TEMPO: 122
ROWS_PER_BEAT: 4
ORDER: 0 0 1 1 2 3 1 0

INST kick: triangle 0.5 0.001 0.09 0.0 0.02 0.75
INST stab: pulse 0.25 0.001 0.04 0.0 0.03 0.13

PATTERN 0
  000 | 0:C-1:kick 1:D-2:bass
  001 | ---
  002 | 2:C-5:hat
  003 | ---
```

Event format: `channel:note:instrument[:volume]`

## Dependencies

| Package | Required | Purpose |
|---------|----------|---------|
| `numpy` | Yes | Sample generation |
| `sounddevice` | Optional | Audio output |
| `libportaudio2` | Optional | System lib for sounddevice |

Audio is gracefully disabled if sounddevice/portaudio are missing.

Install all: `make installer-deps`

## Waveform Reference

| Waveform | Character | Best for |
|----------|-----------|----------|
| `pulse` | Bright, buzzy | Leads, stabs, arps |
| `square` | Hollow | Leads (pulse duty=0.5) |
| `triangle` | Soft, warm | Bass, kicks |
| `saw` | Harsh, edgy | Bass, aggressive leads |
| `noise` | White noise | Hi-hats, snares, claps |

### Pulse Width

| `duty_cycle` | Sound |
|-------------|-------|
| `0.5` | Square wave (most hollow) |
| `0.25` | Classic lead |
| `0.125` | Thin, nasal |
| `0.0625` | Very thin, almost click |

### Detune

Adding `"detune": 0.003` to an instrument:
- Spawns a second voice per note, slightly pitch-shifted
- Creates stereo-like width and harmonic thickness
- Essential for chord stabs to sound full rather than thin
- Costs 1 extra voice per note (budget accordingly with 20-voice pool)
