Skip to content

Commit

Permalink
Merge pull request #17 from braheezy/multi-file-playback
Browse files Browse the repository at this point in the history
Multi file playback
  • Loading branch information
braheezy authored Mar 6, 2024
2 parents 65625d5 + 840c050 commit b3bdf3b
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 44 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ Usage:
Available Commands:
convert Convert between QOA and other audio formats
help Help about any command
play Play a .qoa audio file
play Play .qoa audio file(s)
version Print the version

Flags:
-h, --help help for goqoa
-q, --quiet Suppress command output
-v, --verbose Increase command output

Use "goqoa [command] --help" for more information about a command.
Use "goqoa help [command]" for more information about a command.
```

[This blog post](https://phoboslab.org/log/2023/02/qoa-time-domain-audio-compression) by the author of QOA is a great introduction to the format and how it works.
Expand Down
10 changes: 2 additions & 8 deletions cmd/goqoa.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var version = "2.1.0"
var version = "2.2.0"

var rootCmd = &cobra.Command{
Use: "goqoa",
Short: "A simple QOA utility.",
Long: "A CLI tool to play and convert QOA audio files.",
Run: func(cmd *cobra.Command, args []string) {
// Display help when no subcommand is provided
fmt.Println("Usage: goqoa [command]")
fmt.Println("Use 'goqoa help' for a list of commands.")
},
Args: cobra.NoArgs,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
setupLogger()
},
Expand Down
85 changes: 51 additions & 34 deletions cmd/play.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"io"
"os"
"time"

Expand All @@ -11,29 +12,19 @@ import (

var playCmd = &cobra.Command{
Use: "play <input-file>",
Short: "Play a .qoa audio file",
Args: cobra.ExactArgs(1),
Short: "Play .qoa audio file(s)",
Long: "Provide one or more QOA files to play.",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
inputFile := args[0]
playQOA(inputFile)
playQOA(args[0:])
},
}

func init() {
rootCmd.AddCommand(playCmd)
}

func playQOA(inputFile string) {
qoaBytes, err := os.ReadFile(inputFile)
if err != nil {
logger.Fatalf("Error reading QOA file: %v", err)
}

// Decode the QOA audio data
_, qoaAudioData, err := qoa.Decode(qoaBytes)
if err != nil {
logger.Fatalf("Error decoding QOA data: %v", err)
}
func playQOA(inputFiles []string) {

// Prepare an Oto context (this will use your default audio device)
ctx, ready, err := oto.NewContext(
Expand All @@ -46,25 +37,45 @@ func playQOA(inputFile string) {
panic("oto.NewContext failed: " + err.Error())
}

// Wait for the context to be ready
<-ready

// Create a new player with the custom QOAAudioReader
player := ctx.NewPlayer(NewQOAAudioReader(qoaAudioData))

// Play the audio
logger.Debug("Starting audio...", "SampleRate", "44100", "ChannelCount", "2")
player.Play()

// player.IsPlaying() is the recommended approach but it never returns false for us.
// This method of checking the unplayed buffer size also works.
for player.BufferedSize() != 0 {
time.Sleep(time.Millisecond)
}

// Close the player
if err := player.Close(); err != nil {
logger.Fatalf("Error closing player: %v", err)
for _, inputFile := range inputFiles {
qoaBytes, err := os.ReadFile(inputFile)
if err != nil {
logger.Fatalf("Error reading QOA file: %v", err)
}

// Decode the QOA audio data
_, qoaAudioData, err := qoa.Decode(qoaBytes)
if err != nil {
logger.Fatalf("Error decoding QOA data: %v", err)
}

// Wait for the context to be ready
<-ready

// Create a new player with the custom QOAAudioReader
player := ctx.NewPlayer(NewQOAAudioReader(qoaAudioData))

// Play the audio
logger.Debug(
"Starting audio",
"File",
inputFile,
"SampleRate",
"44100",
"ChannelCount",
"2",
"BufferedSize",
player.BufferedSize())
player.Play()

for player.IsPlaying() {
time.Sleep(time.Millisecond)
}

// Close the player
if err := player.Close(); err != nil {
logger.Fatalf("Error closing player: %v", err)
}
}
}

Expand All @@ -84,6 +95,12 @@ type QOAAudioReader struct {

func (r *QOAAudioReader) Read(p []byte) (n int, err error) {
samplesToRead := len(p) / 2

if r.pos >= len(r.data) {
// Return EOF when there is no more data to read
return 0, io.EOF
}

if samplesToRead > len(r.data)-r.pos {
samplesToRead = len(r.data) - r.pos
}
Expand Down

0 comments on commit b3bdf3b

Please sign in to comment.