-
Notifications
You must be signed in to change notification settings - Fork 0
Sound
There are two sound-related types:
-
PlayerOne.Sound
: A table that defines when and how a sound plays. A theme is consist of theseSound
. -
PlayerOne.SoundParams
: The actual sound parameters (frequency, wave type, etc.)
A Sound
is consist of:
-
event
: when to play -
sound
: what to play -
callback
: how to play (optional)
When loading a theme, player-one.nvim
will iterate through each Sound
and bind them to the desired event
with the callback
provided.
---@class PlayerOne.Sound
{
---@type string Event name that triggers the sound (see `:h events`)
event = "",
---@type PlayerOne.SoundParams|PlayerOne.SoundParams[] Sound parameters
sound = {},
---@type string|function Callback to execute when sound plays
---@default "play"
callback = "play",
}
This field specifies the name of the autocmd event that will trigger a command. Autocmd events are hooks that execute commands when certain actions occur in the editor. For example, events such as BufRead
, BufWrite
, or InsertEnter
can be used to run specific commands automatically when a buffer is read, written, or when entering insert mode, respectively.
For a complete list of events and further details on their behavior, see :h event
.
You can create SoundParams
in two formats:
- Using a Lua table with real units
- Using a JSON string from jsfxr
You can create a sound with real units as a Lua table, for example:
local coin = {
wave_type = 1, -- Sawtooth wave
env_sustain = 0.001, -- 1ms sustain
env_punch = 45.72, -- 45.72% punch
env_decay = 0.26, -- 260ms decay
base_freq = 1071.0, -- 1071Hz (approximately C6)
arp_mod = 1.343, -- Frequency multiplier for arpeggio
arp_speed = 0.044, -- Arpeggio speed in seconds
duty = 50.0, -- Square wave duty cycle
lpf_ramp = 1.0, -- Linear increase in filter cutoff
lpf_resonance = 45.0, -- Filter resonance percentage
}
require("player-one").play(coin)
jsfxr is an online 8 bit sound maker and sfx generator. It offers an easy way to generate and preview a sound. You can use it to generate sounds to your liking. And then press the Serialize button, copy the json shown and used it directly in your table.
For example:
local coin = [[{
"oldParams": true,
"wave_type": 1,
"p_env_attack": 0,
"p_env_sustain": 0.024,
"p_env_punch": 0.457,
"p_env_decay": 0.342,
"p_base_freq": 0.550,
"p_arp_mod": 0.532,
"p_arp_speed": 0.689,
"sound_vol": 0.25
}]]
require("player-one").play(coin)
You can include multiple sounds in one sound table. Then assign different callbacks so that it can either be played all at once as a chord or in sequence like a melody.
Here's an example:
sound = {
{
wave_type = 1,
base_freq = 261.63,
env_decay = 0.1,
},
{
wave_type = 1,
base_freq = 329.63,
env_decay = 0.1,
},
{
wave_type = 1,
base_freq = 392.00,
env_decay = 0.1,
}
}
Note
- Some
json
params has a prefix ofp_
- The table uses real units while json uses normalized values (0.0-1.0)
Parameter | Unit | Description | Default |
---|---|---|---|
wave_type | int | 0: Square, 1: Sawtooth, 2: Sine, 3: Noise, 4: Triangle | 0 |
env_attack | sec | Time to reach peak volume | 0.3628 |
env_sustain | sec | Time to hold peak volume | 0.0227 |
env_punch | +% | Additional volume boost at the start | 0.0 |
env_decay | sec | Time to fade to silence | 0.5669 |
base_freq | Hz | Base frequency of the sound | 321.0 |
freq_limit | Hz | Minimum frequency during slides | 0.0 |
freq_ramp | 8va/sec | Frequency change over time (octaves per second) | 0.0 |
freq_dramp | 8va/s^2 | Change in frequency slide (octaves per second^2) | 0.0 |
vib_strength | ± % | Vibrato depth | 0.0 |
vib_speed | Hz | Vibrato frequency | 0.0 |
arp_mod | mult | Frequency multiplier for arpeggio | 0.0 |
arp_speed | sec | Time between arpeggio notes | 0.0 |
duty | % | Square wave duty cycle (wave_type = 0 only) | 50.0 |
duty_ramp | %/sec | Change in duty cycle over time | 0.0 |
repeat_speed | Hz | Sound repeat frequency | 0.0 |
pha_offset | msec | Phaser offset | 0.0 |
pha_ramp | msec/sec | Change in phaser offset over time | 0.0 |
lpf_freq | Hz | Low-pass filter cutoff frequency | 0.0 |
lpf_ramp | ^sec | Change in filter cutoff over time | 0.0 |
lpf_resonance | % | Filter resonance | 45.0 |
hpf_freq | Hz | High-pass filter cutoff frequency | 0.0 |
hpf_ramp | ^sec | Change in high-pass filter cutoff over time | 0.0 |
The callback determine the what happen when the event(autocmd) is triggered, for example, you may want to play
a beep when you move the cursor. player-one.nvim
provides three different ways to play sounds: play
, append
, play_async
A quick comparison:
Mode | Interrupts Current | Queues Sounds | Blocks Execution |
---|---|---|---|
play |
✅ | ❌ | ❌ |
append |
❌ | ✅ | ❌ |
play_async |
✅ | ✅ | ✅ |
Plays the sound immediately, interrupting any currently playing sounds. Best for immediate feedback or creating a chord.
-
Create a simple beep when typing:
{ event = "TextChangedI", sound = { wave_type = 1, base_freq = 440.0, env_decay = 0.05, }, callback = "play" -- Immediate feedback }
-
Play a C major chord (C4, E4, G4):
{ event = "InsertEnter", sound = { -- C4 (261.63 Hz) { wave_type = 1, base_freq = 261.63, env_decay = 0.1, }, -- E4 (329.63 Hz) { wave_type = 1, base_freq = 329.63, env_decay = 0.1, }, -- G4 (392.00 Hz) { wave_type = 1, base_freq = 392.00, env_decay = 0.1, } }, callback = "play" -- All notes play simultaneously }
Queues sounds to play sequentially. Perfect for creating melodies or sequences.
-- Startup melody with multiple notes
{
event = "VimEnter",
sound = {
{ wave_type = 1, base_freq = 523.25 },
{ wave_type = 1, base_freq = 659.25 },
{ wave_type = 1, base_freq = 783.99 },
},
callback = "append" -- Notes play in sequence
}
Plays the sound and waits for it to complete before continuing. Useful for confirmations or alerts.
-- Save confirmation with chord
{
event = "BufWritePost",
sound = {
{ wave_type = 1, base_freq = 587.33 },
{ wave_type = 1, base_freq = 880.00 },
},
callback = "play_async" -- Wait for completion
}
-
Use
play
for:- Immediate feedback (typing, cursor movement)
- Single sound effects
- Overlapping sounds
-
Use
append
for:- Musical sequences
- Multi-note melodies
- Sound effect combinations
-
Use
play_async
for:- Confirmation sounds
- Operation completion alerts
- Synchronized audio feedback
For simple sound playback without additional logic, you can use the function name as a string shorthand. If a callback is not provided, it will use "play" as default. Here are two equivalent approaches:
-
Example
- Using string shorthand
-- This configuration will play a simple beep sound when text changes in insert mode { event = "TextChangedI", -- Triggers when text is changed in insert mode sound = { wave_type = 1, base_freq = 440.0, env_decay = 0.05, }, callback = "play" -- String shorthand for simple playback }
- Using explicit callback function, This does exactly the same thing as Example 1, but with a custom callback
{ event = "TextChangedI", sound = { wave_type = 1, base_freq = 440.0, env_decay = 0.05, }, callback = function(sound) require('player-one').play(sound) -- Explicitly calling the play function end }
In addition to the built-in callbacks ("play"
, "append"
, "play_async"
), you can define custom callback functions for more complex sound behaviors.
local player_one = require("player-one")
local utils = require("player-one.utils")
---@type PlayerOne.Theme
local theme = {
-- Example 1: Conditional Sound Based on Buffer Type
{
event = "BufWritePost",
sound = {
{ wave_type = 1, base_freq = 587.33, env_decay = 0.15 }, -- D5
{ wave_type = 1, base_freq = 880.00, env_decay = 0.15 }, -- A5
},
callback = function(sound)
-- Play different sounds for different file types
local ft = vim.bo.filetype
if ft == "lua" then
utils.append(sound)
elseif ft == "rust" then
utils.play_async(sound)
else
utils.play(sound)
end
end
},
-- Example 2: Dynamic Sound Parameters
{
event = "CursorMoved",
sound = {
wave_type = 1,
base_freq = 440.0,
env_decay = 0.05,
},
callback = function(sound)
-- Modify frequency based on cursor position
local pos = vim.api.nvim_win_get_cursor(0)
local line = pos[1]
local col = pos[2]
-- Adjust frequency based on position
sound.base_freq = 440.0 + (line % 12) * 50
-- Only play if enabled and after delay
if vim.g.player_one ~= false then
utils.play(sound)
end
end
},
-- Example 3: Sequential Sounds with Delay
{
event = "VimEnter",
sound = {
{ wave_type = 1, base_freq = 523.25, env_decay = 0.15 }, -- C5
{ wave_type = 1, base_freq = 659.25, env_decay = 0.15 }, -- E5
{ wave_type = 1, base_freq = 783.99, env_decay = 0.15 }, -- G5
},
callback = function(sound)
-- Play startup sound after a delay
vim.defer_fn(function()
utils.append(sound)
end, 1000) -- 1 second delay
end
},
-- Example 4: Volume Based on Window Size
{
event = "VimResized",
sound = {
wave_type = 2,
base_freq = 440.0,
env_decay = 0.1,
},
callback = function(sound)
-- Adjust volume based on window width
local width = vim.api.nvim_win_get_width(0)
sound.sound_vol = math.min(width / 100, 1.0)
utils.play(sound)
end
}
}
-- Apply the theme
player_one.setup({
theme = theme
})
Custom callbacks give you full control over:
- When and how sounds are played
- Sound parameter modifications
- Conditional playback logic
- Integration with Neovim state
- Complex sound sequences
- Timing and delays
Tips for Custom Callbacks:
- Use
utils.play()
,utils.append()
, orutils.play_async()
for sound playback - Check
vim.g.player_one
for global enable state - Modify sound parameters before playback
- Use
vim.defer_fn()
for delayed playback - Access Neovim API for context-aware sounds
local theme = {
-- Immediate feedback for typing
{
event = "TextChangedI",
sound = { wave_type = 1, base_freq = 440.0 },
callback = "play"
},
-- Musical sequence for startup
{
event = "VimEnter",
sound = {
{ wave_type = 1, base_freq = 523.25 },
{ wave_type = 1, base_freq = 659.25 },
{ wave_type = 1, base_freq = 783.99 },
},
callback = "append"
},
-- Confirmation for save
{
event = "BufWritePost",
sound = {
{ wave_type = 1, base_freq = 587.33 },
{ wave_type = 1, base_freq = 880.00 },
},
callback = "play_async"
}
}