Compare commits
10 Commits
3981512edb
...
d59a2859b6
Author | SHA1 | Date |
---|---|---|
filifa | d59a2859b6 | |
filifa | afb198052e | |
filifa | 6f5f8b1818 | |
filifa | 3882ffe4f7 | |
filifa | 23cb23de15 | |
filifa | 54f1665325 | |
filifa | 82e2b05925 | |
filifa | 9d16b2970c | |
filifa | 6410b5ee6f | |
filifa | 9e01387460 |
|
@ -22,9 +22,12 @@ generate the PCM stream, then pipe to milkbucket, like so:
|
|||
```
|
||||
ffmpeg -i $audio -ar 44100 -f s16le - | ./milkbucket -p $preset
|
||||
```
|
||||
Note that you can pass in multiple presets with multiple `-p` flags, then use
|
||||
the arrow keys to cycle through the presets while running.
|
||||
|
||||
Note that neither of these commands will output any audio! If you want to hear
|
||||
the audio at the same time (and assuming your machine uses pipewire), run:
|
||||
Note that neither of these commands (ffmpeg or milkbucket) will output any
|
||||
audio! If you want to hear the audio at the same time (and assuming your
|
||||
machine uses pipewire), run:
|
||||
```
|
||||
ffmpeg -i $audio -ar 44100 -f s16le - | tee >(pw-play --rate=44100 --format=s16 -) | ./milkbucket -p $preset
|
||||
```
|
||||
|
|
109
cmd/root.go
109
cmd/root.go
|
@ -17,6 +17,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
|
@ -27,6 +30,41 @@ import (
|
|||
var presets []string
|
||||
var transition bool
|
||||
|
||||
/*
|
||||
* checkStdin checks if any data has been passed in through stdin and returns
|
||||
* an error if we don't believe so.
|
||||
*/
|
||||
func checkStdin() error {
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||
return errors.New("nothing to read from stdin")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* validatePresets performs some basic checks on the presets passed in and
|
||||
* returns an error if it finds a problem.
|
||||
*/
|
||||
func validatePresets() error {
|
||||
for _, p := range presets {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if info.IsDir() {
|
||||
return errors.New("preset " + p + " is a directory")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* handleWindowEvent handles window events like resizing.
|
||||
*/
|
||||
func handleWindowEvent(event *sdl.WindowEvent, m *milkDropWindow) {
|
||||
switch event.Event {
|
||||
case sdl.WINDOWEVENT_RESIZED:
|
||||
|
@ -35,6 +73,9 @@ func handleWindowEvent(event *sdl.WindowEvent, m *milkDropWindow) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* handleKeyboardEvent handles keyboard inputs.
|
||||
*/
|
||||
func handleKeyboardEvent(event *sdl.KeyboardEvent, m *milkDropWindow) {
|
||||
scancode := event.Keysym.Scancode
|
||||
if event.Type == sdl.KEYDOWN && scancode == sdl.SCANCODE_RIGHT && event.Repeat == 0 {
|
||||
|
@ -44,6 +85,10 @@ func handleKeyboardEvent(event *sdl.KeyboardEvent, m *milkDropWindow) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* handleEvent passes the event to a more specific function based on its type.
|
||||
* It returns false if the program should quit, true otherwise.
|
||||
*/
|
||||
func handleEvent(event sdl.Event, m *milkDropWindow) bool {
|
||||
switch event.(type) {
|
||||
case *sdl.QuitEvent:
|
||||
|
@ -59,32 +104,55 @@ func handleEvent(event sdl.Event, m *milkDropWindow) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
* update handles events and renders new frames of the visualization. It
|
||||
* returns a bool indicating whether the program should keep running and an
|
||||
* error, if any.
|
||||
*/
|
||||
func update(m *milkDropWindow) (bool, error) {
|
||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||
keepRunning := handleEvent(event, m)
|
||||
if !keepRunning {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
audioData := make([]int16, maxSamples(2, 2))
|
||||
|
||||
err := binary.Read(os.Stdin, binary.LittleEndian, audioData)
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
m.render(audioData)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* milkbucket sets up the program and starts a rendering loop.
|
||||
*/
|
||||
func milkbucket(cmd *cobra.Command, args []string) {
|
||||
stat, err := os.Stdin.Stat()
|
||||
err := checkStdin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||
log.Fatal("nothing to read from stdin")
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = sdl.Init(sdl.INIT_VIDEO)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer sdl.Quit()
|
||||
|
||||
for _, p := range presets {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if info.IsDir() {
|
||||
panic("preset " + p + " is a directory")
|
||||
}
|
||||
err = validatePresets()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
m, err := newMilkDropWindow(800, 600, presets)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer m.destroy()
|
||||
|
||||
|
@ -92,20 +160,9 @@ func milkbucket(cmd *cobra.Command, args []string) {
|
|||
|
||||
running := true
|
||||
for running {
|
||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||
running = handleEvent(event, m)
|
||||
if !running {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !running {
|
||||
break
|
||||
}
|
||||
|
||||
running, err = m.render()
|
||||
running, err = update(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,17 +25,25 @@ import "C"
|
|||
|
||||
import (
|
||||
"container/ring"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
const bufSize uint = 2048
|
||||
/*
|
||||
* maxSamples returns the maximum number of audio samples stored by projectm
|
||||
* when passing it audio data, given the number of bytes in one sample and the
|
||||
* number of channels.
|
||||
*/
|
||||
func maxSamples(bytesPerSample uint, numChannels uint) uint {
|
||||
// NOTE: not 100% sure about this, but I think
|
||||
// projectm_pcm_get_max_samples assumes 4 bytes per sample, so we
|
||||
// multiply by 4/bytesPerSample to convert
|
||||
return 4 / bytesPerSample * numChannels * uint(C.projectm_pcm_get_max_samples())
|
||||
}
|
||||
|
||||
// milkDropWindow represents the window projectm will render visualizations in.
|
||||
type milkDropWindow struct {
|
||||
window *sdl.Window
|
||||
context sdl.GLContext
|
||||
|
@ -43,6 +51,9 @@ type milkDropWindow struct {
|
|||
preset *ring.Ring
|
||||
}
|
||||
|
||||
/*
|
||||
* setupPresets initializes a ring to store the passed presets in.
|
||||
*/
|
||||
func (m *milkDropWindow) setupPresets(presets []string) {
|
||||
m.preset = ring.New(len(presets))
|
||||
for _, preset := range presets {
|
||||
|
@ -51,16 +62,28 @@ func (m *milkDropWindow) setupPresets(presets []string) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* prevPreset sets the milkDropWindow's preset to the one after the current
|
||||
* preset.
|
||||
*/
|
||||
func (m *milkDropWindow) nextPreset(smooth bool) {
|
||||
m.preset = m.preset.Next()
|
||||
m.loadPreset(smooth)
|
||||
}
|
||||
|
||||
/*
|
||||
* prevPreset sets the milkDropWindow's preset to the one before the current
|
||||
* preset.
|
||||
*/
|
||||
func (m *milkDropWindow) prevPreset(smooth bool) {
|
||||
m.preset = m.preset.Prev()
|
||||
m.loadPreset(smooth)
|
||||
}
|
||||
|
||||
/*
|
||||
* newMilkDropWindow returns a new milkDropWindow with the given width and
|
||||
* height, and sets presets available to the window.
|
||||
*/
|
||||
func newMilkDropWindow(width, height int32, presets []string) (*milkDropWindow, error) {
|
||||
var m milkDropWindow
|
||||
var err error
|
||||
|
@ -87,12 +110,20 @@ func newMilkDropWindow(width, height int32, presets []string) (*milkDropWindow,
|
|||
return &m, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* destroy handles cleanup of the milkDropWindow.
|
||||
*/
|
||||
func (m *milkDropWindow) destroy() {
|
||||
C.projectm_destroy(m.handle)
|
||||
sdl.GLDeleteContext(m.context)
|
||||
m.window.Destroy()
|
||||
}
|
||||
|
||||
/*
|
||||
* loadPreset loads the preset file currently pointed to by m.preset. smooth
|
||||
* determines whether there is a smooth transition between the current preset
|
||||
* and the new preset.
|
||||
*/
|
||||
func (m *milkDropWindow) loadPreset(smooth bool) {
|
||||
preset := m.preset.Value.(string)
|
||||
cPreset := C.CString(preset)
|
||||
|
@ -100,24 +131,20 @@ func (m *milkDropWindow) loadPreset(smooth bool) {
|
|||
C.projectm_load_preset_file(m.handle, cPreset, C.bool(smooth))
|
||||
}
|
||||
|
||||
func (m *milkDropWindow) render() (bool, error) {
|
||||
var audioData [bufSize]int16
|
||||
|
||||
err := binary.Read(os.Stdin, binary.LittleEndian, &audioData)
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
C.projectm_pcm_add_int16(m.handle, (*C.int16_t)(&audioData[0]), C.uint(bufSize), C.PROJECTM_STEREO)
|
||||
/*
|
||||
* render passes data to projectm for rendering. It returns a bool for if the
|
||||
* program should keep running and an error.
|
||||
*/
|
||||
func (m *milkDropWindow) render(data []int16) {
|
||||
C.projectm_pcm_add_int16(m.handle, (*C.int16_t)(&data[0]), C.uint(len(data)), C.PROJECTM_STEREO)
|
||||
|
||||
C.projectm_opengl_render_frame(m.handle)
|
||||
m.window.GLSwap()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* resize informs projectm of the new window size.
|
||||
*/
|
||||
func (m *milkDropWindow) resize(w, h int32) {
|
||||
C.projectm_set_window_size(m.handle, C.ulong(w), C.ulong(h))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue