Compare commits

..

No commits in common. "d59a2859b61c3a18c1bf5dc4016bcfedec421dcc" and "3981512edbfbbef30619d1fb4f8af5b547898898" have entirely different histories.

3 changed files with 45 additions and 132 deletions

View File

@ -22,12 +22,9 @@ 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 (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:
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:
```
ffmpeg -i $audio -ar 44100 -f s16le - | tee >(pw-play --rate=44100 --format=s16 -) | ./milkbucket -p $preset
```

View File

@ -17,9 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"encoding/binary"
"errors"
"io"
"log"
"os"
@ -30,41 +27,6 @@ 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:
@ -73,9 +35,6 @@ 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 {
@ -85,10 +44,6 @@ 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:
@ -104,55 +59,32 @@ 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) {
err := checkStdin()
stat, err := os.Stdin.Stat()
if err != nil {
log.Fatal(err)
panic(err)
} else if (stat.Mode() & os.ModeCharDevice) != 0 {
log.Fatal("nothing to read from stdin")
}
err = sdl.Init(sdl.INIT_VIDEO)
if err != nil {
log.Fatal(err)
panic(err)
}
defer sdl.Quit()
err = validatePresets()
for _, p := range presets {
info, err := os.Stat(p)
if err != nil {
log.Fatal(err)
panic(err)
} else if info.IsDir() {
panic("preset " + p + " is a directory")
}
}
m, err := newMilkDropWindow(800, 600, presets)
if err != nil {
log.Fatal(err)
panic(err)
}
defer m.destroy()
@ -160,9 +92,20 @@ func milkbucket(cmd *cobra.Command, args []string) {
running := true
for running {
running, err = update(m)
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
running = handleEvent(event, m)
if !running {
break
}
}
if !running {
break
}
running, err = m.render()
if err != nil {
log.Fatal(err)
panic(err)
}
}
}

View File

@ -25,25 +25,17 @@ import "C"
import (
"container/ring"
"encoding/binary"
"errors"
"io"
"os"
"unsafe"
"github.com/veandco/go-sdl2/sdl"
)
/*
* 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())
}
const bufSize uint = 2048
// milkDropWindow represents the window projectm will render visualizations in.
type milkDropWindow struct {
window *sdl.Window
context sdl.GLContext
@ -51,9 +43,6 @@ 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 {
@ -62,28 +51,16 @@ 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
@ -110,20 +87,12 @@ 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)
@ -131,20 +100,24 @@ func (m *milkDropWindow) loadPreset(smooth bool) {
C.projectm_load_preset_file(m.handle, cPreset, C.bool(smooth))
}
/*
* 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)
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)
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))
}