Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 159 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,59 @@
```
T E X T S T E P
```
<div align="center">

# T E X T S T E P

**A terminal-based step sequencer, drum machine, and synthesizer built entirely in Rust.**

A terminal-based step sequencer, drum machine, and synthesizer built in Rust.
All DSP from scratch — no samples, no external audio libraries. Just your terminal and your speakers.

[![Rust](https://img.shields.io/badge/Rust-1.70%2B-orange?logo=rust&logoColor=white)](https://www.rust-lang.org/)
[![License: GPL v2](https://img.shields.io/badge/License-GPL_v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[![Platform: macOS](https://img.shields.io/badge/Platform-macOS-lightgrey?logo=apple&logoColor=white)](https://www.apple.com/macos/)
[![Audio: CoreAudio](https://img.shields.io/badge/Audio-CoreAudio-green)](https://developer.apple.com/documentation/coreaudio)
[![TUI: ratatui](https://img.shields.io/badge/TUI-ratatui-purple)](https://ratatui.rs/)
[![Lines of Code](https://img.shields.io/badge/Lines_of_Code-17k-informational)]()
[![Tests](https://img.shields.io/badge/Tests-23_passing-brightgreen)]()

![TextStep Demo](assets/demo.gif)

## What Is This
</div>

---

TextStep is a TUI music production tool with:
## Features

- **8 drum tracks** — Kick, Snare, Closed HiHat, Open HiHat, Ride, Clap, Cowbell, Tom — each fully synthesized with 8 tweakable sound parameters
- **Polyphonic synth** — 2 oscillators + sub, 2 ADSR envelopes, resonant filter, LFO with 6 waveforms, collapsible UI section
- **32-step sequencer** with 10 patterns and 8 kit slots
- **Send effects chain** — Schroeder reverb, tempo-synced filtered delay, tube saturator, SSL-style glue compressor
- **Live performance** — drum pads, real-time recording, pattern queuing, per-pattern BPM
- **Mouse support** — click the grid, drag parameters Ableton-style, audition sounds from the activity bar
- **Project system** — save/load `.tsp` files, standalone kit export, preset browser
- **8 Drum Tracks** — Kick, Snare, Closed HiHat, Open HiHat, Ride, Clap, Cowbell, Tom — each fully synthesized with 8 tweakable sound parameters
- **Polyphonic Synth** — 2 oscillators + sub, 2 ADSR envelopes, resonant filter, LFO with 6 waveforms, collapsible UI section
- **32-Step Sequencer** — 10 patterns and 8 kit slots with per-pattern BPM and swing
- **Send Effects Chain** — Schroeder reverb, tempo-synced filtered delay, tube saturator, SSL-style glue compressor
- **Live Performance** — drum pads, real-time recording, pattern queuing, per-pattern BPM
- **Mouse Support** — click the grid, drag parameters Ableton-style, audition sounds from the activity bar
- **Project System** — save/load `.tsp` files, standalone kit export, preset browser
- **Spectrum Analyzer** — real-time FFT spectrum display and VU meter with 90s Hi-Fi LED aesthetic

Ships with **10 demo patterns** ready to play: House, Chicago House, Brit House, French House, Dirty House, Trance, Techno, Drum & Bass, Trap, and Moombahton.

### Color Palette
## Getting Started

Hardware/synthwave aesthetic — amber for data, cyan for transport, pink for focus, gold for queued state. All rendered with UTF-8 block characters on a dark background.
### Prerequisites

## Build & Run
- [Rust](https://www.rust-lang.org/tools/install) 1.70 or later
- macOS with CoreAudio (primary target)

### Build & Run
Requires Rust (1.70+). macOS with CoreAudio is the primary target; Linux with ALSA is also supported.

```bash
# Clone the repository
git clone https://github.com/illobo/textStep.git
cd textStep

# Build and run (release mode recommended for audio performance)
cargo build --release
cargo run --release
```

### Run Tests
On Linux, install ALSA development headers first:

```bash
Expand All @@ -43,7 +63,7 @@ sudo apt-get install libasound2-dev # Debian/Ubuntu
Run the tests:

```bash
cargo test
cargo test # 23 tests, runs in <1s
```

### Pre-built Binaries
Expand Down Expand Up @@ -145,7 +165,28 @@ Projects are stored as JSON in `~/Library/Application Support/textstep/projects/

## Architecture

Two-thread model:
```
┌──────────────────────────────────────────────────┐
│ UI Thread │
│ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ ratatui │ │ crossterm│ │ App State │ │
│ │ renderer │ │ events │ │ (app.rs) │ │
│ └────────────┘ └──────────┘ └──────────────┘ │
│ │
│ crossbeam channels (lock-free, bounded) │
│ ▼ UiToAudio ▲ AudioToUi │
│ │
│ Audio Thread │
│ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Sequencer │ │ Voices │ │ Effects │ │
│ │ Clock │ │ Drum+Syn │ │ Rev/Dly/Comp │ │
│ └────────────┘ └──────────┘ └──────────────┘ │
│ │ │
│ cpal/CoreAudio │
└──────────────────────────────────────────────────┘
```

**Two-thread model:**

- **UI thread** — ratatui + crossterm for rendering and input at ~60fps
- **Audio thread** — cpal/CoreAudio callback running all DSP per-sample
Expand All @@ -156,25 +197,112 @@ Communication is lock-free via bounded crossbeam channels. The audio thread neve

Every sound is synthesized in real-time with no external DSP dependencies:

- **Drum voices**: TR-808/909-inspired kicks (sine + pitch envelope + resonant impulse), noise-blended snares, 6-oscillator metallic banks for hats and rides (using Mutable Instruments Plaits-style inharmonic ratios), ring-modulated open hats, bandpass claps, detuned pulse cowbells, FM toms
- **Synth voice**: dual oscillators, sub, noise, two ADSR envelopes, resonant SVF filter, 6-waveform LFO
- **Effects**: Schroeder/Freeverb reverb (4 comb + 2 allpass), tempo-synced filtered delay, asymmetric tube saturator, feedforward RMS glue compressor with soft knee
- **Primitives**: 1-pole HP/LP filters, state-variable filter, xorshift32 noise, tanh waveshaping
- **Drum voices** — TR-808/909-inspired kicks (sine + pitch envelope + resonant impulse), noise-blended snares, 6-oscillator metallic banks for hats and rides (Mutable Instruments Plaits-style inharmonic ratios), ring-modulated open hats, bandpass claps, detuned pulse cowbells, FM toms
- **Synth voice** — dual oscillators, sub, noise, two ADSR envelopes, resonant SVF filter, 6-waveform LFO
- **Effects** — Schroeder/Freeverb reverb (4 comb + 2 allpass), tempo-synced filtered delay, asymmetric tube saturator, feedforward RMS glue compressor with soft knee
- **Primitives** — 1-pole HP/LP filters, state-variable filter, xorshift32 noise, tanh waveshaping

### Source Map

| Directory | Purpose |
|-----------|---------|
| `src/` | Core: entry point, app state, input handling, messages |
| `src/ui/` | Rendering: layout, theme, grids, knobs, transport, spectrum |
| `src/audio/` | DSP: engine, clock, drum/synth voices, effects, mixer, FFT |
| `src/sequencer/` | Data: patterns, transport, project serialization |
| `src/presets/` | Preset browser: drum/synth sounds and patterns by genre |

### Color Palette

Hardware/synthwave aesthetic — all rendered with UTF-8 block characters on a dark background.

| Color | Hex | Usage |
|-------|-----|-------|
| Amber | `#e8a838` | Active steps, gauge fills, primary data |
| Cyan | `#61dafb` | Transport state, beat LEDs, playhead, focused borders |
| Pink | `#ff6b9d` | Focus/selection, current track, record |
| Gold | `#ffd700` | Queued patterns, warnings |

## Dependencies

| Crate | Version | Purpose |
|-------|---------|---------|
| `ratatui` | 0.29 | TUI rendering framework |
| `crossterm` | 0.28 | Terminal backend (events, raw mode) |
| `cpal` | 0.15 | Cross-platform audio I/O (CoreAudio) |
| `crossbeam-channel` | 0.5 | Lock-free bounded MPSC channels |
| `serde` + `serde_json` | 1 | Project serialization (JSON) |

No other runtime dependencies.

### Dependencies
## Project Structure

```
ratatui 0.29 TUI rendering
crossterm 0.28 Terminal backend
cpal 0.15 Audio I/O (CoreAudio)
crossbeam-channel 0.5 Lock-free channels
serde + serde_json Project serialization
textStep/
├── src/
│ ├── main.rs # Entry point, thread spawning, event loop
│ ├── app.rs # Application state, focus, modals
│ ├── keys.rs # Keyboard input handler
│ ├── mouse.rs # Mouse input handler (mirrors UI layout)
│ ├── messages.rs # Cross-thread message enums
│ ├── params.rs # Shared effect parameters
│ ├── ui/
│ │ ├── mod.rs # Top-level render dispatch
│ │ ├── layout.rs # Layout constants (single source of truth)
│ │ ├── theme.rs # Color palette and styling
│ │ ├── transport_bar.rs # Transport controls rendering
│ │ ├── drum_grid.rs # 8×32 drum step grid
│ │ ├── synth_grid.rs # Synth step grid with note display
│ │ ├── knobs.rs # Drum parameter sliders
│ │ ├── synth_knobs.rs # Synth parameter groups
│ │ ├── waveform.rs # Spectrum analyzer / VU meter
│ │ ├── splash.rs # Boot animation
│ │ └── help_overlay.rs # Keyboard shortcut reference
│ ├── audio/
│ │ ├── engine.rs # Audio callback and voice management
│ │ ├── clock.rs # Beat/step timing with swing
│ │ ├── drum_voice.rs # 8 drum synth voices (all DSP)
│ │ ├── synth_voice.rs # Polyphonic synth DSP
│ │ ├── effects.rs # Reverb, delay, compressor, saturator
│ │ ├── mixer.rs # Channel mixing, mute/solo
│ │ ├── display_buffer.rs # Lock-free audio→UI waveform buffer
│ │ └── fft.rs # FFT for spectrum analyzer
│ ├── sequencer/
│ │ ├── drum_pattern.rs # Drum pattern data (8 tracks × 32 steps)
│ │ ├── synth_pattern.rs # Synth pattern data and parameters
│ │ ├── transport.rs # Transport state (BPM, play, swing)
│ │ └── project.rs # Project serialization (.tsp JSON)
│ └── presets/
│ ├── mod.rs # Preset browser state machine
│ ├── drum_presets.rs # Drum sound presets
│ ├── synth_presets.rs # Synth sound presets
│ ├── pattern_presets.rs # Drum pattern presets by genre
│ └── synth_pattern_presets.rs # Synth pattern presets
├── assets/
│ └── demo.gif # Demo recording
├── Cargo.toml
├── BLUEPRINT.md # Full technical documentation
└── LICENSE # GPL v2
```

No other runtime dependencies.
## Contributing

Contributions are welcome! Please ensure:

See [BLUEPRINT.md](BLUEPRINT.md) for full technical documentation.
1. All tests pass: `cargo test`
2. Code compiles without warnings: `cargo build --release`
3. Follow the existing code style and comment conventions

See [BLUEPRINT.md](BLUEPRINT.md) for full technical documentation and architecture details.

## License

[GNU General Public License v2.0](LICENSE)
This project is licensed under the [GNU General Public License v2.0](LICENSE).

---

<div align="center">

**Built with Rust** · **All DSP from scratch** · **Zero audio dependencies**

</div>
5 changes: 3 additions & 2 deletions src/audio/drum_voice.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Drum voice trait and all 8 voice implementations
// Hand-rolled DSP — no external crate dependencies.
//! Drum voice DSP implementations (8 tracks).
//! All synthesis is hand-rolled: oscillators, filters, envelopes, noise.
//! No external DSP crate dependencies.

use crate::sequencer::drum_pattern::DrumTrackParams;

Expand Down
11 changes: 4 additions & 7 deletions src/audio/effects.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// Audio effects: Reverb, Delay, Glue Compressor
// Reverb/Delay ported from zicbox applyReverb.h with adaptations for Rust.
//! Send effects: Schroeder reverb, tempo-synced delay, tube saturator, RMS glue compressor.
//! Reverb/delay ported from zicbox applyReverb.h with adaptations for Rust.

// ---------------------------------------------------------------------------
// Reverb: 4 parallel comb filters + 2 series allpass filters
// ---------------------------------------------------------------------------

// Base lengths tuned for 44100 Hz; scaled by sample_rate / 44100.0
// Base comb/allpass lengths tuned for 44100 Hz; scaled by sample_rate / 44100.0
const BASE_COMB_LENGTHS: [usize; 4] = [1117, 1301, 1571, 1787];
const BASE_ALLPASS_LENGTHS: [usize; 2] = [557, 443];

/// Schroeder reverb: 4 parallel comb filters feeding 2 series allpass filters.
pub struct ReverbEffect {
comb_buf: Vec<f32>,
comb_lengths: [usize; 4],
Expand Down
9 changes: 5 additions & 4 deletions src/audio/engine.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// AudioEngine: cpal callback, voice mixing, send effects, message handling
//! Audio engine: cpal callback, voice management, LFO, message dispatch.

use std::sync::Arc;

Expand All @@ -16,9 +16,8 @@ use crate::sequencer::drum_pattern::{DrumPattern, DrumTrackId, NUM_DRUM_TRACKS};
use crate::sequencer::synth_pattern::{SynthPattern, LFO_DEST_FIELDS, lfo_division_multiplier};
use crate::sequencer::transport::{PlayState, Transport};

// ---------------------------------------------------------------------------
// LFO: tempo-synced, global (shared across synth voices)
// ---------------------------------------------------------------------------
/// Tempo-synced global LFO shared across synth voices.
/// Supports sine, triangle, saw (up/down), square, and exponential decay waveforms.
struct Lfo {
phase: f64,
}
Expand Down Expand Up @@ -76,6 +75,8 @@ impl Lfo {
}
}

/// Core audio engine running on the audio thread.
/// Owns all voices, effects, the sequencer clock, and handles messages from the UI thread.
pub struct AudioEngine {
sample_rate: f64,
clock: SequencerClock,
Expand Down
9 changes: 4 additions & 5 deletions src/audio/synth_voice.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Paraphonic 2-oscillator synthesizer voice with sub oscillator.
// Hand-rolled DSP no external crate dependencies.
//! Polyphonic synth voice: dual oscillators, sub, noise, 2 ADSR envelopes,
//! SVF filter, tempo-synced LFO. Hand-rolled DSP, no external crate dependencies.

use crate::sequencer::synth_pattern::SynthParams;

Expand Down Expand Up @@ -368,9 +368,8 @@ impl Filter24dB {
}
}

// ---------------------------------------------------------------------------
// SynthVoice — paraphonic 2-oscillator + sub
// ---------------------------------------------------------------------------
/// Paraphonic synthesizer voice with dual oscillators, sub oscillator,
/// two ADSR envelopes (amplitude + filter), and a state-variable filter.
pub struct SynthVoice {
sample_rate: f32,
osc1: Oscillator,
Expand Down
3 changes: 2 additions & 1 deletion src/keys.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Keyboard input mapping
//! Keyboard event handler: maps key events to app state changes.

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

Expand All @@ -10,6 +10,7 @@ use crate::sequencer::project::{NUM_KITS, NUM_PATTERNS};
use crate::sequencer::synth_pattern::{SynthControlField, MAX_STEPS as SYNTH_MAX_STEPS};
use crate::sequencer::transport::{PlayState, RecordMode};

/// How much a parameter changes per arrow-key press (0.02 = 50 steps for full range).
const PARAM_INCREMENT: f32 = 0.02;

/// Map drum pad keys (bottom row) to track indices.
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Entry point: spawns audio thread, runs UI event loop with ~60fps rendering.

mod app;
mod audio;
mod keys;
Expand All @@ -18,6 +20,9 @@ use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;

/// Sets up cross-thread channels, starts the audio stream, initializes the
/// terminal, and enters the main UI loop. The audio stream is kept alive
/// until this function returns.
fn main() -> io::Result<()> {
// Create channels for UI <-> Audio communication
let (tx_to_audio, rx_from_ui) = crossbeam_channel::bounded(64);
Expand Down
6 changes: 5 additions & 1 deletion src/messages.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// UiToAudio and AudioToUi message enums
//! Lock-free message types for cross-thread communication between UI and audio.

use crate::params::EffectParams;
use crate::sequencer::drum_pattern::{DrumPattern, DrumTrackId};
use crate::sequencer::synth_pattern::SynthPattern;
use crate::sequencer::transport::Transport;

/// Commands sent from the UI thread to the audio thread.
/// Sent via a bounded crossbeam channel (capacity 64).
pub enum UiToAudio {
SetTransport(Transport),
SetDrumPattern(DrumPattern),
Expand All @@ -15,6 +17,8 @@ pub enum UiToAudio {
ReleaseSynth, // release synth envelopes
}

/// Notifications sent from the audio thread back to the UI.
/// Sent via a bounded crossbeam channel (capacity 16).
pub enum AudioToUi {
PlaybackPosition {
global_step: usize,
Expand Down
6 changes: 4 additions & 2 deletions src/mouse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Mouse input handling
//! Mouse event handler. Hit-testing layout MUST mirror ui/mod.rs — uses shared
//! constants from ui/layout.rs.

use std::time::Instant;

Expand All @@ -18,7 +19,8 @@ const DOUBLE_CLICK_MS: u128 = 300;
/// 0.04 = full range in ~25 rows of drag.
const DRAG_SENSITIVITY: f32 = 0.04;

/// Main mouse event handler.
/// Routes mouse events (click, scroll, drag) to the appropriate UI section.
/// Layout rectangles are recomputed from `term_size` to match rendering.
pub fn handle_mouse(app: &mut App, event: MouseEvent, term_size: Rect) {
// Ignore mouse during splash or modals
if app.ui.splash.phase != crate::app::SplashPhase::Done {
Expand Down
4 changes: 3 additions & 1 deletion src/params.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Shared parameter types
//! Shared effect parameters used by both UI and audio engine.

use serde::{Deserialize, Serialize};

/// Master effect and mix bus parameters, serialized with each project.
/// All values are normalized to 0.0..1.0.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct EffectParams {
pub reverb_amount: f32, // 0.0-1.0: feedback/decay
Expand Down
Loading
Loading