Skip to content
Jack Huang edited this page Feb 14, 2025 · 5 revisions

There are two sound-related types:

  1. PlayerOne.Sound: A table that defines when and how a sound plays. A theme is consist of these Sound.

  2. PlayerOne.SoundParams: The actual sound parameters (frequency, wave type, etc.)

PlayerOne.Sound

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",
}

Event

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.

PlayerOne.SoundParams

You can create SoundParams in two formats:

  1. Using a Lua table with real units
  2. Using a JSON string from jsfxr

Using Lua Table

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)

Using JSON

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)

Multiple Sounds

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,
      }
    }

Sound Parameters

Note

  1. Some json params has a prefix of p_
  2. 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

Callback

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

play(sound)

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
    }

append(sound)

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
}

play_async(sound)

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 Cases

  • 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

Callback Shorthand

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

    1. 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
    }
    1. 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
    }

Custom Callbacks

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:

  1. Use utils.play(), utils.append(), or utils.play_async() for sound playback
  2. Check vim.g.player_one for global enable state
  3. Modify sound parameters before playback
  4. Use vim.defer_fn() for delayed playback
  5. Access Neovim API for context-aware sounds

Example

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"
  }
}