mirror of
https://github.com/alda-lang/alda.git
synced 2026-02-27 18:24:13 +01:00
io/ioutil has been deprecated since Go 1.16 https://pkg.go.dev/io/ioutil Signed-off-by: ginglis13 <ginglis05@gmail.com>
262 lines
5.7 KiB
Go
262 lines
5.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"alda.io/client/color"
|
|
"alda.io/client/help"
|
|
log "alda.io/client/logging"
|
|
"alda.io/client/model"
|
|
"alda.io/client/parser"
|
|
"alda.io/client/system"
|
|
"alda.io/client/transmitter"
|
|
"alda.io/client/util"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const midiExportTimeout = 60 * time.Second
|
|
|
|
var outputFilename string
|
|
var outputFormat string
|
|
|
|
func init() {
|
|
exportCmd.Flags().StringVarP(
|
|
&file, "file", "f", "", "Read Alda source code from a file",
|
|
)
|
|
|
|
exportCmd.Flags().StringVarP(
|
|
&code, "code", "c", "", "Supply Alda source code as a string",
|
|
)
|
|
|
|
exportCmd.Flags().StringVarP(
|
|
&optionFrom,
|
|
"from",
|
|
"F",
|
|
"",
|
|
"A time marking (e.g. 0:30) or marker from which to start",
|
|
)
|
|
|
|
exportCmd.Flags().StringVarP(
|
|
&optionTo,
|
|
"to",
|
|
"T",
|
|
"",
|
|
"A time marking (e.g. 1:00) or marker at which to end",
|
|
)
|
|
|
|
exportCmd.Flags().StringVarP(
|
|
&outputFilename, "output", "o", "", "The output filename",
|
|
)
|
|
|
|
exportCmd.Flags().StringVarP(
|
|
&outputFormat, "output-format", "O", "midi", "The output format",
|
|
)
|
|
}
|
|
|
|
var exportCmd = &cobra.Command{
|
|
Use: "export",
|
|
Short: "Evaluate Alda source code and export to another format",
|
|
Long: fmt.Sprintf(`Evaluate Alda source code and export to another format
|
|
|
|
---
|
|
|
|
%s
|
|
|
|
---
|
|
|
|
When -o / --output FILENAME is provided, the results are written into that file.
|
|
|
|
alda export -c "piano: c d e" -o three-notes.mid
|
|
|
|
Otherwise, the results are written to stdout, which is convenient for
|
|
redirecting into other files or processes.
|
|
|
|
alda export -c "piano: c d e" > three-notes.mid
|
|
alda export -c "piano: c d e" | some-process > three-notes-processed.mid
|
|
|
|
---
|
|
|
|
Currently, the only output format is MIDI. At some point, there will be other
|
|
output formats like MusicXML.
|
|
|
|
---`,
|
|
sourceCodeInputOptions("export", false),
|
|
),
|
|
RunE: func(_ *cobra.Command, args []string) error {
|
|
if outputFormat != "midi" {
|
|
return help.UserFacingErrorf(
|
|
`%s is not a supported output format.
|
|
|
|
Currently, the only supported output format is %s.`,
|
|
color.Aurora.BrightYellow(outputFormat),
|
|
color.Aurora.BrightYellow("midi"),
|
|
)
|
|
}
|
|
|
|
var ast parser.ASTNode
|
|
var scoreUpdates []model.ScoreUpdate
|
|
var err error
|
|
|
|
switch {
|
|
case file != "":
|
|
ast, err = parser.ParseFile(file)
|
|
|
|
case code != "":
|
|
ast, err = parser.ParseString(code)
|
|
|
|
default:
|
|
ast, err = parseStdin()
|
|
}
|
|
|
|
if err == system.ErrNoInputSupplied {
|
|
return userFacingNoInputSuppliedError("export")
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
scoreUpdates, err = ast.Updates()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
score := model.NewScore()
|
|
start := time.Now()
|
|
if err := score.Update(scoreUpdates...); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info().
|
|
Int("updates", len(scoreUpdates)).
|
|
Str("took", time.Since(start).String()).
|
|
Msg("Constructed score.")
|
|
|
|
var player system.PlayerState
|
|
|
|
// Find an available player process to use.
|
|
system.StartingPlayerProcesses()
|
|
if err := util.Await(
|
|
func() error {
|
|
foundPlayer, err := system.FindAvailablePlayer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
player = foundPlayer
|
|
return nil
|
|
},
|
|
reasonableTimeout,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
transmitOpts := []transmitter.TransmissionOption{
|
|
transmitter.TransmitFrom(optionFrom),
|
|
transmitter.TransmitTo(optionTo),
|
|
transmitter.LoadOnly(),
|
|
}
|
|
|
|
transmitter := transmitter.OSCTransmitter{Port: player.Port}
|
|
|
|
if err := transmitter.TransmitScore(score, transmitOpts...); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info().
|
|
Interface("player", player).
|
|
Msg("Transmitted score to player.")
|
|
|
|
// Ensure that the player process doesn't hang around with the score loaded.
|
|
// This avoids an unexpected behavior where if you run `alda export ...`
|
|
// followed by `alda play` without arguments (i.e. to un-pause playback),
|
|
// you end up hearing the score that was loaded into the player process that
|
|
// was used for the export. The desired behavior is that we shut the player
|
|
// down after using it for this one-off export.
|
|
defer func() {
|
|
transmitter.TransmitShutdownMessage(0)
|
|
|
|
log.Info().
|
|
Interface("player", player).
|
|
Msg("Sent shutdown message to player.")
|
|
}()
|
|
|
|
// When no output filename is specified, we write the result to stdout. But
|
|
// first, we need to ask the player to export to a file, so we use a
|
|
// temporary file.
|
|
tmpFilename := ""
|
|
if outputFilename == "" {
|
|
tmpdir, err := os.MkdirTemp("", "alda-export")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tmpFilename = filepath.Join(
|
|
tmpdir, fmt.Sprintf(
|
|
"export-%d-%d.mid",
|
|
time.Now().Unix(),
|
|
rand.Intn(10000),
|
|
),
|
|
)
|
|
}
|
|
|
|
var targetFilename string
|
|
if outputFilename != "" {
|
|
targetFilename = outputFilename
|
|
} else {
|
|
targetFilename = tmpFilename
|
|
}
|
|
|
|
err = transmitter.TransmitMidiExportMessage(targetFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info().
|
|
Interface("player", player).
|
|
Msg("Transmitted \"export\" message.")
|
|
|
|
log.Debug().
|
|
Str("targetFilename", targetFilename).
|
|
Msg("Waiting for target file to be written.")
|
|
|
|
// There can be a noticeable delay while we wait for the player process to
|
|
// finish writing the target file. So, we display a message here to give the
|
|
// user some incremental feedback and avoid making it look like Alda is
|
|
// "hanging."
|
|
fmt.Fprintln(os.Stderr, "Exporting...")
|
|
|
|
var midiFile *os.File
|
|
|
|
if err := util.Await(
|
|
func() error {
|
|
mf, err := os.Open(targetFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
midiFile = mf
|
|
return nil
|
|
},
|
|
midiExportTimeout,
|
|
); err != nil {
|
|
return nil
|
|
}
|
|
|
|
if outputFilename != "" {
|
|
fmt.Fprintf(os.Stderr, "Exported score to %s\n", outputFilename)
|
|
} else {
|
|
if _, err := io.Copy(os.Stdout, midiFile); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|