6 Commits

Author SHA1 Message Date
ae5a1f2218 chore(deps): update Go to 1.26, bump purego, fix lint warning (#15)
- Updated Go version from 1.24.0 to 1.26.0
- Bumped ebitengine/purego v0.9.1 -> v0.10.0
- Removed dead code in SynthesisOptions.Clamp() (staticcheck S1002)
2026-03-04 04:45:43 -05:00
7e25beb0a7 chore: update dependencies, fix vet warning (#14)
- Bump go directive from 1.23 to 1.24
- Update ebitengine/oto/v3 v3.3.2 → v3.4.0
- Update ebitengine/purego v0.8.0 → v0.9.1
- Update golang.org/x/sys v0.32.0 → v0.41.0
- Fix fmt.Sprintf with non-format string in stt.go
2026-02-22 20:51:27 -05:00
b179ed7944 update to newer sound package 2025-07-04 19:22:42 -07:00
c5609d07e9 update dep 2025-04-30 10:26:33 -07:00
Jose Ramirez
d79325ad71 add WithHTTPClient (#13) 2025-04-30 10:21:50 -07:00
fba03ed0be update error code for 400 2025-03-13 20:39:03 -07:00
13 changed files with 86 additions and 156 deletions

View File

@@ -1,10 +1,10 @@
# elevenlabs
[![License 0BSD](https://img.shields.io/badge/License-0BSD-pink.svg)](https://opensource.org/licenses/0BSD)
[![GoDoc](https://godoc.org/github.com/taigrr/elevenlabs?status.svg)](https://godoc.org/github.com/taigrr/elevenlabs)
[![Go Mod](https://img.shields.io/badge/go.mod-v1.20-blue)](go.mod)
[![Go Report Card](https://goreportcard.com/badge/github.com/taigrr/elevenlabs?branch=master)](https://goreportcard.com/report/github.com/taigrr/elevenlabs)
Unofficial [elevenlabs.io](https://beta.elevenlabs.io/) ([11.ai](http://11.ai)) voice synthesis client
This library is not affiliated with, nor associated with ElevenLabs in any way.
@@ -13,10 +13,10 @@ ElevenLabs' official api documentation, upon which this client has been
derived, [can be found here](https://api.elevenlabs.io/docs).
## Purpose
This go client provides an easy interface to create synthesized voices and
make TTS (text-to-speech) requests to elevenlabs.io
As a prerequisite, you must already have an account with elevenlabs.io.
After creating your account, you can get your API key [from here](https://help.elevenlabs.io/hc/en-us/articles/14599447207697-How-to-authorize-yourself-using-your-xi-api-key-).
@@ -35,8 +35,9 @@ Set the `XI_API_KEY` environment variable, and pipe it some text to give it a wh
To use this library, create a new client and send a TTS request to a voice.
The following code block illustrates how one might replicate the say/espeak
command, using the streaming endpoint.
I've opted to go with faiface's beep package, but you can also save the file
I've opted to go with gopxl's beep package, but you can also save the file
to an mp3 on-disk.
```go
package main
@@ -48,9 +49,9 @@ import (
"os"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker"
"github.com/taigrr/elevenlabs/client"
"github.com/taigrr/elevenlabs/client/types"

View File

@@ -2,6 +2,7 @@ package client
import (
"errors"
"net/http"
)
const apiEndpoint = "https://api.elevenlabs.io"
@@ -12,14 +13,16 @@ var (
)
type Client struct {
apiKey string
endpoint string
apiKey string
endpoint string
httpClient *http.Client
}
func New(apiKey string) Client {
return Client{
apiKey: apiKey,
endpoint: apiEndpoint,
apiKey: apiKey,
endpoint: apiEndpoint,
httpClient: &http.Client{},
}
}
@@ -27,3 +30,14 @@ func (c Client) WithEndpoint(endpoint string) Client {
c.endpoint = endpoint
return c
}
func (c Client) WithAPIKey(apiKey string) Client {
c.apiKey = apiKey
return c
}
// WithHTTPClient allows users to provide their own http.Client
func (c Client) WithHTTPClient(hc *http.Client) Client {
c.httpClient = hc
return c
}

View File

@@ -15,8 +15,6 @@ import (
func (c Client) HistoryDelete(ctx context.Context, historyItemID string) (bool, error) {
url := fmt.Sprintf(c.endpoint+"/v1/history/%s", historyItemID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return false, err
@@ -24,7 +22,7 @@ func (c Client) HistoryDelete(ctx context.Context, historyItemID string) (bool,
req.Header.Set("accept", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
@@ -55,7 +53,6 @@ func (c Client) HistoryDownloadZipWriter(ctx context.Context, w io.Writer, id1,
toDownload := types.HistoryPost{
HistoryItemIds: downloads,
}
client := &http.Client{}
body, _ := json.Marshal(toDownload)
bodyReader := bytes.NewReader(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
@@ -66,7 +63,7 @@ func (c Client) HistoryDownloadZipWriter(ctx context.Context, w io.Writer, id1,
req.Header.Set("accept", "archive/zip")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
@@ -99,7 +96,6 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
toDownload := types.HistoryPost{
HistoryItemIds: downloads,
}
client := &http.Client{}
body, _ := json.Marshal(toDownload)
bodyReader := bytes.NewReader(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
@@ -110,7 +106,7 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
req.Header.Set("accept", "archive/zip")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
@@ -142,7 +138,6 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
func (c Client) HistoryDownloadAudioWriter(ctx context.Context, w io.Writer, ID string) error {
url := fmt.Sprintf(c.endpoint+"/v1/history/%s/audio", ID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
@@ -150,7 +145,7 @@ func (c Client) HistoryDownloadAudioWriter(ctx context.Context, w io.Writer, ID
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -179,7 +174,6 @@ func (c Client) HistoryDownloadAudioWriter(ctx context.Context, w io.Writer, ID
func (c Client) HistoryDownloadAudio(ctx context.Context, ID string) ([]byte, error) {
url := fmt.Sprintf(c.endpoint+"/v1/history/%s/audio", ID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []byte{}, err
@@ -187,7 +181,7 @@ func (c Client) HistoryDownloadAudio(ctx context.Context, ID string) ([]byte, er
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []byte{}, err
}
@@ -219,7 +213,6 @@ func (c Client) HistoryDownloadAudio(ctx context.Context, ID string) ([]byte, er
func (c Client) GetHistoryItemList(ctx context.Context) ([]types.HistoryItemList, error) {
url := c.endpoint + "/v1/history"
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []types.HistoryItemList{}, err
@@ -227,7 +220,7 @@ func (c Client) GetHistoryItemList(ctx context.Context) ([]types.HistoryItemList
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []types.HistoryItemList{}, err
}

View File

@@ -15,7 +15,6 @@ import (
func (c Client) DeleteVoiceSample(ctx context.Context, voiceID, sampleID string) (bool, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/samples/%s", voiceID, sampleID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return false, err
@@ -24,7 +23,7 @@ func (c Client) DeleteVoiceSample(ctx context.Context, voiceID, sampleID string)
req.Header.Set("accept", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return false, err
}
@@ -50,7 +49,6 @@ func (c Client) DeleteVoiceSample(ctx context.Context, voiceID, sampleID string)
func (c Client) DownloadVoiceSampleWriter(ctx context.Context, w io.Writer, voiceID, sampleID string) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/samples/%s/audio", voiceID, sampleID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
@@ -58,7 +56,7 @@ func (c Client) DownloadVoiceSampleWriter(ctx context.Context, w io.Writer, voic
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -87,7 +85,6 @@ func (c Client) DownloadVoiceSampleWriter(ctx context.Context, w io.Writer, voic
func (c Client) DownloadVoiceSample(ctx context.Context, voiceID, sampleID string) ([]byte, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/samples/%s/audio", voiceID, sampleID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []byte{}, err
@@ -95,7 +92,7 @@ func (c Client) DownloadVoiceSample(ctx context.Context, voiceID, sampleID strin
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []byte{}, err
}

View File

@@ -68,8 +68,6 @@ func (c Client) SoundGeneration(ctx context.Context, text string, durationSecond
func (c Client) requestSoundGeneration(ctx context.Context, params types.SoundGeneration) (io.ReadCloser, error) {
url := c.endpoint + "/v1/sound-generation"
client := &http.Client{}
b, err := json.Marshal(params)
if err != nil {
return nil, err
@@ -84,7 +82,7 @@ func (c Client) requestSoundGeneration(ctx context.Context, params types.SoundGe
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

View File

@@ -75,8 +75,7 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
}
client := &http.Client{}
url := fmt.Sprintf(c.endpoint + "/v1/speech-to-text")
url := c.endpoint + "/v1/speech-to-text"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
@@ -86,7 +85,7 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("xi-api-key", c.apiKey)
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
@@ -102,6 +101,16 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
return &sttResponse, nil
case 422:
ve := types.ValidationError{}
defer res.Body.Close()
jerr := json.NewDecoder(res.Body).Decode(&ve)
if jerr != nil {
err = errors.Join(err, jerr)
} else {
err = errors.Join(err, ve)
}
return nil, err
case 400:
fallthrough
default:
ve := types.ParamError{}

View File

@@ -88,7 +88,6 @@ func (c Client) requestTTS(ctx context.Context, params types.TTS, options types.
if params.Stream {
url += "/stream"
}
client := &http.Client{}
b, _ := json.Marshal(params)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(b))
if err != nil {
@@ -97,7 +96,7 @@ func (c Client) requestTTS(ctx context.Context, params types.TTS, options types.
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

View File

@@ -40,9 +40,8 @@ func (so *SynthesisOptions) Clamp() {
if so.Style > 1 || so.Style < 0 {
so.Style = 0.0
}
if so.UseSpeakerBoost != true && so.UseSpeakerBoost != false {
so.UseSpeakerBoost = true
}
// UseSpeakerBoost defaults to the zero value (false) for bool;
// no conditional needed as the caller sets it explicitly.
}
type SynthesisOptions struct {

View File

@@ -11,7 +11,6 @@ import (
func (c Client) GetUserInfo(ctx context.Context) (types.UserResponseModel, error) {
url := c.endpoint + "/v1/user"
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return types.UserResponseModel{}, err
@@ -19,7 +18,7 @@ func (c Client) GetUserInfo(ctx context.Context) (types.UserResponseModel, error
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return types.UserResponseModel{}, err
}

View File

@@ -17,8 +17,6 @@ import (
func (c Client) CreateVoice(ctx context.Context, name, description string, labels []string, files []*os.File) error {
url := c.endpoint + "/v1/voices/add"
client := &http.Client{}
var b bytes.Buffer
w := multipart.NewWriter(&b)
for _, r := range files {
@@ -43,7 +41,7 @@ func (c Client) CreateVoice(ctx context.Context, name, description string, label
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -69,7 +67,6 @@ func (c Client) CreateVoice(ctx context.Context, name, description string, label
func (c Client) DeleteVoice(ctx context.Context, voiceID string) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s", voiceID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return err
@@ -77,7 +74,7 @@ func (c Client) DeleteVoice(ctx context.Context, voiceID string) error {
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -103,7 +100,7 @@ func (c Client) DeleteVoice(ctx context.Context, voiceID string) error {
func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings types.SynthesisOptions) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/settings/edit", voiceID)
client := &http.Client{}
b, _ := json.Marshal(settings)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
@@ -113,7 +110,7 @@ func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -145,7 +142,6 @@ func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings
func (c Client) EditVoice(ctx context.Context, voiceID, name, description string, labels []string, files []*os.File) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/edit", voiceID)
client := &http.Client{}
var b bytes.Buffer
w := multipart.NewWriter(&b)
@@ -171,7 +167,7 @@ func (c Client) EditVoice(ctx context.Context, voiceID, name, description string
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -197,7 +193,7 @@ func (c Client) EditVoice(ctx context.Context, voiceID, name, description string
func (c Client) GetVoiceSettings(ctx context.Context, voiceID string) (types.SynthesisOptions, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/settings", voiceID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return types.SynthesisOptions{}, err
@@ -205,7 +201,7 @@ func (c Client) GetVoiceSettings(ctx context.Context, voiceID string) (types.Syn
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return types.SynthesisOptions{}, err
}
@@ -237,7 +233,7 @@ func (c Client) GetVoiceSettings(ctx context.Context, voiceID string) (types.Syn
func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceResponseModel, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s", voiceID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return types.VoiceResponseModel{}, err
@@ -245,7 +241,7 @@ func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceRespon
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
return types.VoiceResponseModel{}, ErrUnauthorized
@@ -279,7 +275,7 @@ func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceRespon
func (c Client) GetVoices(ctx context.Context) ([]types.VoiceResponseModel, error) {
url := c.endpoint + "/v1/voices"
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []types.VoiceResponseModel{}, err
@@ -287,7 +283,7 @@ func (c Client) GetVoices(ctx context.Context) ([]types.VoiceResponseModel, erro
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
return []types.VoiceResponseModel{}, ErrUnauthorized

View File

@@ -9,9 +9,9 @@ import (
"strings"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker"
"github.com/taigrr/elevenlabs/client"
"github.com/taigrr/elevenlabs/client/types"

15
go.mod
View File

@@ -1,18 +1,13 @@
module github.com/taigrr/elevenlabs
go 1.23.0
go 1.26.0
toolchain go1.24.0
require github.com/faiface/beep v1.1.0
require github.com/gopxl/beep/v2 v2.1.1
require (
github.com/ebitengine/oto/v3 v3.4.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
github.com/hajimehoshi/oto v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/exp/shiny v0.0.0-20250228200357-dead58393ab7 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)

102
go.sum
View File

@@ -1,92 +1,22 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gopxl/beep/v2 v2.1.1 h1:6FYIYMm2qPAdWkjX+7xwKrViS1x0Po5kDMdRkq8NVbU=
github.com/gopxl/beep/v2 v2.1.1/go.mod h1:ZAm9TGQ9lvpoiFLd4zf5B1IuyxZhgRACMId1XJbaW0E=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4=
github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp/shiny v0.0.0-20250228200357-dead58393ab7 h1:VxTRg3kpOpYQ+S2PlDH9x2j/ZOQMxVsPgdYYRvkErNY=
golang.org/x/exp/shiny v0.0.0-20250228200357-dead58393ab7/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20230906132913-2077a3224571 h1:QDvQ2KLFHHQWRID6IkZOBf6uLIh9tZ0G+mw61pFQxuo=
golang.org/x/mobile v0.0.0-20230906132913-2077a3224571/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4=
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3 h1:0V/7Y1FEaFdAzb9DkVDh4QFp4vL4yYCiJ5cjk80lZyA=
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3/go.mod h1:j5VYNgQ6lZYZlzHFjdgS2UeqRSZunDk+/zXVTAIA3z4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=