Remove leftover static assets specific to individual blog posts
This commit is contained in:
parent
0bc9cd83b4
commit
c4ec906406
@ -1,314 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger describes a simple component used for printing log lines.
|
||||
type Logger interface {
|
||||
Printf(string, ...interface{})
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The scoreboard component
|
||||
|
||||
// File wraps the standard os.File type.
|
||||
type File interface {
|
||||
io.ReadWriter
|
||||
Truncate(int64) error
|
||||
Seek(int64, int) (int64, error)
|
||||
}
|
||||
|
||||
// scoreboard loads player scores from a save file, tracks score updates, and
|
||||
// periodically saves those scores back to the save file.
|
||||
type scoreboard struct {
|
||||
file File
|
||||
scoresM map[string]int
|
||||
scoresLock sync.Mutex
|
||||
|
||||
// this field will only be set in tests, and is used to synchronize with the
|
||||
// the for-select loop in saveLoop.
|
||||
saveLoopWaitCh chan struct{}
|
||||
}
|
||||
|
||||
// newScoreboard initializes a scoreboard using scores saved in the given File
|
||||
// (which may be empty). The scoreboard will rewrite the save file with the
|
||||
// latest scores everytime saveTicker is written to.
|
||||
func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger) (*scoreboard, error) {
|
||||
fileBody, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading saved scored: %w", err)
|
||||
}
|
||||
|
||||
scoresM := map[string]int{}
|
||||
if len(fileBody) > 0 {
|
||||
if err := json.Unmarshal(fileBody, &scoresM); err != nil {
|
||||
return nil, fmt.Errorf("decoding saved scores: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scoreboard := &scoreboard{
|
||||
file: file,
|
||||
scoresM: scoresM,
|
||||
saveLoopWaitCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go scoreboard.saveLoop(saveTicker, logger)
|
||||
|
||||
return scoreboard, nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) guessedCorrect(name string) int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
s.scoresM[name] += 1000
|
||||
return s.scoresM[name]
|
||||
}
|
||||
|
||||
func (s *scoreboard) guessedIncorrect(name string) int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
s.scoresM[name] -= 1
|
||||
return s.scoresM[name]
|
||||
}
|
||||
|
||||
func (s *scoreboard) scores() map[string]int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
scoresCp := map[string]int{}
|
||||
for name, score := range s.scoresM {
|
||||
scoresCp[name] = score
|
||||
}
|
||||
|
||||
return scoresCp
|
||||
}
|
||||
|
||||
func (s *scoreboard) save() error {
|
||||
scores := s.scores()
|
||||
if _, err := s.file.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("seeking to start of save file: %w", err)
|
||||
} else if err := s.file.Truncate(0); err != nil {
|
||||
return fmt.Errorf("truncating save file: %w", err)
|
||||
} else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
|
||||
return fmt.Errorf("encoding scores to save file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
if err := s.save(); err != nil {
|
||||
logger.Printf("error saving scoreboard to file: %v", err)
|
||||
}
|
||||
case <-s.saveLoopWaitCh:
|
||||
// test will unblock, nothing to do here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The httpHandlers component
|
||||
|
||||
// Scoreboard describes the scoreboard component from the point of view of the
|
||||
// httpHandler component (which only needs a subset of scoreboard's methods).
|
||||
type Scoreboard interface {
|
||||
guessedCorrect(name string) int
|
||||
guessedIncorrect(name string) int
|
||||
scores() map[string]int
|
||||
}
|
||||
|
||||
// RandSrc describes a randomness component which can produce random integers.
|
||||
type RandSrc interface {
|
||||
Int() int
|
||||
}
|
||||
|
||||
// httpHandlers implements the http.HandlerFuncs used by the httpServer.
|
||||
type httpHandlers struct {
|
||||
scoreboard Scoreboard
|
||||
randSrc RandSrc
|
||||
logger Logger
|
||||
|
||||
mux *http.ServeMux
|
||||
n int
|
||||
nLock sync.Mutex
|
||||
}
|
||||
|
||||
func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
|
||||
n := randSrc.Int()
|
||||
logger.Printf("first n is %v", n)
|
||||
|
||||
httpHandlers := &httpHandlers{
|
||||
scoreboard: scoreboard,
|
||||
randSrc: randSrc,
|
||||
logger: logger,
|
||||
mux: http.NewServeMux(),
|
||||
n: n,
|
||||
}
|
||||
|
||||
httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
|
||||
httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
|
||||
|
||||
return httpHandlers
|
||||
}
|
||||
|
||||
func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
name := r.FormValue("name")
|
||||
nStr := r.FormValue("n")
|
||||
if name == "" || nStr == "" {
|
||||
http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(nStr)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.nLock.Lock()
|
||||
defer h.nLock.Unlock()
|
||||
|
||||
if h.n == n {
|
||||
newScore := h.scoreboard.guessedCorrect(name)
|
||||
h.n = h.randSrc.Int()
|
||||
h.logger.Printf("new n is %v", h.n)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
|
||||
return
|
||||
}
|
||||
|
||||
hint := "higher"
|
||||
if h.n < n {
|
||||
hint = "lower"
|
||||
}
|
||||
|
||||
newScore := h.scoreboard.guessedIncorrect(name)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
|
||||
}
|
||||
|
||||
func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
h.nLock.Lock()
|
||||
defer h.nLock.Unlock()
|
||||
|
||||
type scoreTup struct {
|
||||
name string
|
||||
score int
|
||||
}
|
||||
|
||||
scores := h.scoreboard.scores()
|
||||
scoresTups := make([]scoreTup, 0, len(scores))
|
||||
for name, score := range scores {
|
||||
scoresTups = append(scoresTups, scoreTup{name, score})
|
||||
}
|
||||
|
||||
sort.Slice(scoresTups, func(i, j int) bool {
|
||||
return scoresTups[i].score > scoresTups[j].score
|
||||
})
|
||||
|
||||
for _, scoresTup := range scoresTups {
|
||||
fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The httpServer component.
|
||||
|
||||
type httpServer struct {
|
||||
httpServer *http.Server
|
||||
errCh chan error
|
||||
}
|
||||
|
||||
func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
|
||||
loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
|
||||
httpHandlers.ServeHTTP(rw, r)
|
||||
})
|
||||
|
||||
server := &httpServer{
|
||||
httpServer: &http.Server{
|
||||
Handler: loggingHandler,
|
||||
},
|
||||
errCh: make(chan error, 1),
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.httpServer.Serve(listener)
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
err = nil
|
||||
}
|
||||
server.errCh <- err
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// main
|
||||
|
||||
const (
|
||||
saveFilePath = "./save.json"
|
||||
listenAddr = ":8888"
|
||||
saveInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||
|
||||
logger.Printf("opening scoreboard save file %q", saveFilePath)
|
||||
file, err := os.OpenFile(saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to open file %q: %v", saveFilePath, err)
|
||||
}
|
||||
|
||||
saveTicker := time.NewTicker(saveInterval)
|
||||
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
logger.Printf("initializing scoreboard")
|
||||
scoreboard, err := newScoreboard(file, saveTicker.C, logger)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to initialize scoreboard: %v", err)
|
||||
}
|
||||
|
||||
logger.Printf("listening on %q", listenAddr)
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to listen on %q: %v", listenAddr, err)
|
||||
}
|
||||
|
||||
logger.Printf("setting up HTTP handlers")
|
||||
httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
|
||||
|
||||
logger.Printf("serving HTTP requests")
|
||||
newHTTPServer(listener, httpHandlers, logger)
|
||||
|
||||
logger.Printf("initialization done")
|
||||
select {} // block forever
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type nullLogger struct{}
|
||||
|
||||
func (nullLogger) Printf(string, ...interface{}) {}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Test scoreboard component
|
||||
|
||||
type fileStub struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func newFileStub(init string) *fileStub {
|
||||
return &fileStub{Buffer: bytes.NewBufferString(init)}
|
||||
}
|
||||
|
||||
func (fs *fileStub) Truncate(i int64) error {
|
||||
fs.Buffer.Truncate(int(i))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fileStub) Seek(i int64, whence int) (int64, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func TestScoreboard(t *testing.T) {
|
||||
newScoreboard := func(t *testing.T, fileStub *fileStub, saveTicker <-chan time.Time) *scoreboard {
|
||||
t.Helper()
|
||||
scoreboard, err := newScoreboard(fileStub, saveTicker, nullLogger{})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error checking saved scored: %v", err)
|
||||
}
|
||||
return scoreboard
|
||||
}
|
||||
|
||||
assertScores := func(t *testing.T, expScores, gotScores map[string]int) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(expScores, gotScores) {
|
||||
t.Errorf("expected scores of %+v, but instead got %+v", expScores, gotScores)
|
||||
}
|
||||
}
|
||||
|
||||
assertSavedScores := func(t *testing.T, expScores map[string]int, fileStub *fileStub) {
|
||||
t.Helper()
|
||||
fileStubCp := newFileStub(fileStub.String())
|
||||
tmpScoreboard := newScoreboard(t, fileStubCp, nil)
|
||||
assertScores(t, expScores, tmpScoreboard.scores())
|
||||
}
|
||||
|
||||
t.Run("loading", func(t *testing.T) {
|
||||
// make sure loading scoreboards with various file contents works
|
||||
assertSavedScores(t, map[string]int{}, newFileStub(""))
|
||||
assertSavedScores(t, map[string]int{"foo": 1}, newFileStub(`{"foo":1}`))
|
||||
assertSavedScores(t, map[string]int{"foo": 1, "bar": -2}, newFileStub(`{"foo":1,"bar":-2}`))
|
||||
})
|
||||
|
||||
t.Run("tracking", func(t *testing.T) {
|
||||
scoreboard := newScoreboard(t, newFileStub(""), nil)
|
||||
assertScores(t, map[string]int{}, scoreboard.scores()) // sanity check
|
||||
|
||||
scoreboard.guessedCorrect("foo")
|
||||
assertScores(t, map[string]int{"foo": 1000}, scoreboard.scores())
|
||||
|
||||
scoreboard.guessedIncorrect("bar")
|
||||
assertScores(t, map[string]int{"foo": 1000, "bar": -1}, scoreboard.scores())
|
||||
|
||||
scoreboard.guessedIncorrect("foo")
|
||||
assertScores(t, map[string]int{"foo": 999, "bar": -1}, scoreboard.scores())
|
||||
})
|
||||
|
||||
t.Run("saving", func(t *testing.T) {
|
||||
// this test tests scoreboard's periodic save feature using a ticker
|
||||
// channel which will be written to manually. The saveLoopWaitCh is used
|
||||
// here to ensure that each ticker has been fully processed.
|
||||
ticker := make(chan time.Time)
|
||||
fileStub := newFileStub("")
|
||||
scoreboard := newScoreboard(t, fileStub, ticker)
|
||||
|
||||
tick := func() {
|
||||
ticker <- time.Time{}
|
||||
scoreboard.saveLoopWaitCh <- struct{}{}
|
||||
}
|
||||
|
||||
// this should not effect the save file at first
|
||||
scoreboard.guessedCorrect("foo")
|
||||
assertSavedScores(t, map[string]int{}, fileStub)
|
||||
|
||||
// after the ticker the new score should get saved
|
||||
tick()
|
||||
assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
|
||||
|
||||
// ticker again after no changes should save the same thing as before
|
||||
tick()
|
||||
assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
|
||||
|
||||
// buffer a bunch of changes, shouldn't get saved till after tick
|
||||
scoreboard.guessedCorrect("foo")
|
||||
scoreboard.guessedCorrect("bar")
|
||||
scoreboard.guessedCorrect("bar")
|
||||
assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
|
||||
tick()
|
||||
assertSavedScores(t, map[string]int{"foo": 2000, "bar": 2000}, fileStub)
|
||||
})
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Test httpHandlers component
|
||||
|
||||
type mockScoreboard map[string]int
|
||||
|
||||
func (mockScoreboard) guessedCorrect(name string) int { return 1 }
|
||||
|
||||
func (mockScoreboard) guessedIncorrect(name string) int { return -1 }
|
||||
|
||||
func (m mockScoreboard) scores() map[string]int { return m }
|
||||
|
||||
type mockRandSrc struct{}
|
||||
|
||||
func (m mockRandSrc) Int() int { return 666 }
|
||||
|
||||
func TestHTTPHandlers(t *testing.T) {
|
||||
mockScoreboard := mockScoreboard{"foo": 1, "bar": 2}
|
||||
httpHandlers := newHTTPHandlers(mockScoreboard, mockRandSrc{}, nullLogger{})
|
||||
|
||||
assertRequest := func(t *testing.T, expCode int, expBody string, r *http.Request) {
|
||||
t.Helper()
|
||||
rw := httptest.NewRecorder()
|
||||
httpHandlers.ServeHTTP(rw, r)
|
||||
if rw.Code != expCode {
|
||||
t.Errorf("expected HTTP response code %d, got %d", expCode, rw.Code)
|
||||
} else if rw.Body.String() != expBody {
|
||||
t.Errorf("expected HTTP response body %q, got %q", expBody, rw.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "/guess?name=foo&n=665", nil)
|
||||
assertRequest(t, 400, "Try higher. Your score is now -1\n", r)
|
||||
|
||||
r = httptest.NewRequest("GET", "/guess?name=foo&n=667", nil)
|
||||
assertRequest(t, 400, "Try lower. Your score is now -1\n", r)
|
||||
|
||||
r = httptest.NewRequest("GET", "/guess?name=foo&n=666", nil)
|
||||
assertRequest(t, 200, "Correct! Your score is now 1\n", r)
|
||||
|
||||
r = httptest.NewRequest("GET", "/scores", nil)
|
||||
assertRequest(t, 200, "bar: 2\nfoo: 1\n", r)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// httpServer is NOT tested, for the following reasons:
|
||||
// * It depends on a `net.Listener`, which is not trivial to mock.
|
||||
// * It does very little besides passing an httpHandlers along to an http.Server
|
||||
// and managing cleanup.
|
||||
// * It isn't likely to be changed often.
|
||||
// * If it were to break it would be very apparent in subsequent testing stages.
|
||||
//
|
@ -1,320 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger describes a simple component used for printing log lines.
|
||||
type Logger interface {
|
||||
Printf(string, ...interface{})
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The scoreboard component
|
||||
|
||||
// File wraps the standard os.File type.
|
||||
type File interface {
|
||||
io.ReadWriter
|
||||
Truncate(int64) error
|
||||
Seek(int64, int) (int64, error)
|
||||
}
|
||||
|
||||
// scoreboard loads player scores from a save file, tracks score updates, and
|
||||
// periodically saves those scores back to the save file.
|
||||
type scoreboard struct {
|
||||
file File
|
||||
scoresM map[string]int
|
||||
scoresLock sync.Mutex
|
||||
|
||||
pointsOnCorrect, pointsOnIncorrect int
|
||||
|
||||
// this field will only be set in tests, and is used to synchronize with the
|
||||
// the for-select loop in saveLoop.
|
||||
saveLoopWaitCh chan struct{}
|
||||
}
|
||||
|
||||
// newScoreboard initializes a scoreboard using scores saved in the given File
|
||||
// (which may be empty). The scoreboard will rewrite the save file with the
|
||||
// latest scores everytime saveTicker is written to.
|
||||
func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger, pointsOnCorrect, pointsOnIncorrect int) (*scoreboard, error) {
|
||||
fileBody, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading saved scored: %w", err)
|
||||
}
|
||||
|
||||
scoresM := map[string]int{}
|
||||
if len(fileBody) > 0 {
|
||||
if err := json.Unmarshal(fileBody, &scoresM); err != nil {
|
||||
return nil, fmt.Errorf("decoding saved scores: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scoreboard := &scoreboard{
|
||||
file: file,
|
||||
scoresM: scoresM,
|
||||
pointsOnCorrect: pointsOnCorrect,
|
||||
pointsOnIncorrect: pointsOnIncorrect,
|
||||
saveLoopWaitCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go scoreboard.saveLoop(saveTicker, logger)
|
||||
|
||||
return scoreboard, nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) guessedCorrect(name string) int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
s.scoresM[name] += s.pointsOnCorrect
|
||||
return s.scoresM[name]
|
||||
}
|
||||
|
||||
func (s *scoreboard) guessedIncorrect(name string) int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
s.scoresM[name] += s.pointsOnIncorrect
|
||||
return s.scoresM[name]
|
||||
}
|
||||
|
||||
func (s *scoreboard) scores() map[string]int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
scoresCp := map[string]int{}
|
||||
for name, score := range s.scoresM {
|
||||
scoresCp[name] = score
|
||||
}
|
||||
|
||||
return scoresCp
|
||||
}
|
||||
|
||||
func (s *scoreboard) save() error {
|
||||
scores := s.scores()
|
||||
if _, err := s.file.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("seeking to start of save file: %w", err)
|
||||
} else if err := s.file.Truncate(0); err != nil {
|
||||
return fmt.Errorf("truncating save file: %w", err)
|
||||
} else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
|
||||
return fmt.Errorf("encoding scores to save file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
if err := s.save(); err != nil {
|
||||
logger.Printf("error saving scoreboard to file: %v", err)
|
||||
}
|
||||
case <-s.saveLoopWaitCh:
|
||||
// test will unblock, nothing to do here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The httpHandlers component
|
||||
|
||||
// Scoreboard describes the scoreboard component from the point of view of the
|
||||
// httpHandler component (which only needs a subset of scoreboard's methods).
|
||||
type Scoreboard interface {
|
||||
guessedCorrect(name string) int
|
||||
guessedIncorrect(name string) int
|
||||
scores() map[string]int
|
||||
}
|
||||
|
||||
// RandSrc describes a randomness component which can produce random integers.
|
||||
type RandSrc interface {
|
||||
Int() int
|
||||
}
|
||||
|
||||
// httpHandlers implements the http.HandlerFuncs used by the httpServer.
|
||||
type httpHandlers struct {
|
||||
scoreboard Scoreboard
|
||||
randSrc RandSrc
|
||||
logger Logger
|
||||
|
||||
mux *http.ServeMux
|
||||
n int
|
||||
nLock sync.Mutex
|
||||
}
|
||||
|
||||
func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
|
||||
n := randSrc.Int()
|
||||
logger.Printf("first n is %v", n)
|
||||
|
||||
httpHandlers := &httpHandlers{
|
||||
scoreboard: scoreboard,
|
||||
randSrc: randSrc,
|
||||
logger: logger,
|
||||
mux: http.NewServeMux(),
|
||||
n: n,
|
||||
}
|
||||
|
||||
httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
|
||||
httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
|
||||
|
||||
return httpHandlers
|
||||
}
|
||||
|
||||
func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
name := r.FormValue("name")
|
||||
nStr := r.FormValue("n")
|
||||
if name == "" || nStr == "" {
|
||||
http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(nStr)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.nLock.Lock()
|
||||
defer h.nLock.Unlock()
|
||||
|
||||
if h.n == n {
|
||||
newScore := h.scoreboard.guessedCorrect(name)
|
||||
h.n = h.randSrc.Int()
|
||||
h.logger.Printf("new n is %v", h.n)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
|
||||
return
|
||||
}
|
||||
|
||||
hint := "higher"
|
||||
if h.n < n {
|
||||
hint = "lower"
|
||||
}
|
||||
|
||||
newScore := h.scoreboard.guessedIncorrect(name)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
|
||||
}
|
||||
|
||||
func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
h.nLock.Lock()
|
||||
defer h.nLock.Unlock()
|
||||
|
||||
type scoreTup struct {
|
||||
name string
|
||||
score int
|
||||
}
|
||||
|
||||
scores := h.scoreboard.scores()
|
||||
scoresTups := make([]scoreTup, 0, len(scores))
|
||||
for name, score := range scores {
|
||||
scoresTups = append(scoresTups, scoreTup{name, score})
|
||||
}
|
||||
|
||||
sort.Slice(scoresTups, func(i, j int) bool {
|
||||
return scoresTups[i].score > scoresTups[j].score
|
||||
})
|
||||
|
||||
for _, scoresTup := range scoresTups {
|
||||
fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The httpServer component.
|
||||
|
||||
type httpServer struct {
|
||||
httpServer *http.Server
|
||||
errCh chan error
|
||||
}
|
||||
|
||||
func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
|
||||
loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
|
||||
httpHandlers.ServeHTTP(rw, r)
|
||||
})
|
||||
|
||||
server := &httpServer{
|
||||
httpServer: &http.Server{
|
||||
Handler: loggingHandler,
|
||||
},
|
||||
errCh: make(chan error, 1),
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.httpServer.Serve(listener)
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
err = nil
|
||||
}
|
||||
server.errCh <- err
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// main
|
||||
|
||||
func main() {
|
||||
saveFilePath := flag.String("save-file", "./save.json", "File used to save scores")
|
||||
listenAddr := flag.String("listen-addr", ":8888", "Address to listen for HTTP requests on")
|
||||
saveInterval := flag.Duration("save-interval", 5*time.Second, "How often to resave scores")
|
||||
pointsOnCorrect := flag.Int("points-on-correct", 1000, "Amount to change a user's score by upon a correct score")
|
||||
pointsOnIncorrect := flag.Int("points-on-incorrect", -1, "Amount to change a user's score by upon an incorrect score")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||
|
||||
logger.Printf("opening scoreboard save file %q", *saveFilePath)
|
||||
file, err := os.OpenFile(*saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to open file %q: %v", *saveFilePath, err)
|
||||
}
|
||||
|
||||
saveTicker := time.NewTicker(*saveInterval)
|
||||
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
logger.Printf("initializing scoreboard")
|
||||
scoreboard, err := newScoreboard(file, saveTicker.C, logger, *pointsOnCorrect, *pointsOnIncorrect)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to initialize scoreboard: %v", err)
|
||||
}
|
||||
|
||||
logger.Printf("listening on %q", *listenAddr)
|
||||
listener, err := net.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to listen on %q: %v", *listenAddr, err)
|
||||
}
|
||||
|
||||
logger.Printf("setting up HTTP handlers")
|
||||
httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
|
||||
|
||||
logger.Printf("serving HTTP requests")
|
||||
newHTTPServer(listener, httpHandlers, logger)
|
||||
|
||||
logger.Printf("initialization done")
|
||||
select {} // block forever
|
||||
}
|
@ -1,390 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger describes a simple component used for printing log lines.
|
||||
type Logger interface {
|
||||
Printf(string, ...interface{})
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The scoreboard component
|
||||
|
||||
// File wraps the standard os.File type.
|
||||
type File interface {
|
||||
io.ReadWriter
|
||||
Truncate(int64) error
|
||||
Seek(int64, int) (int64, error)
|
||||
}
|
||||
|
||||
// scoreboard loads player scores from a save file, tracks score updates, and
|
||||
// periodically saves those scores back to the save file.
|
||||
type scoreboard struct {
|
||||
file File
|
||||
scoresM map[string]int
|
||||
scoresLock sync.Mutex
|
||||
|
||||
pointsOnCorrect, pointsOnIncorrect int
|
||||
|
||||
// The cleanup method closes cleanupCh to signal to all scoreboard's running
|
||||
// go-routines to clean themselves up, and cleanupWG is then used to wait
|
||||
// for those goroutines to do so.
|
||||
cleanupCh chan struct{}
|
||||
cleanupWG sync.WaitGroup
|
||||
|
||||
// this field will only be set in tests, and is used to synchronize with the
|
||||
// the for-select loop in saveLoop.
|
||||
saveLoopWaitCh chan struct{}
|
||||
}
|
||||
|
||||
// newScoreboard initializes a scoreboard using scores saved in the given File
|
||||
// (which may be empty). The scoreboard will rewrite the save file with the
|
||||
// latest scores everytime saveTicker is written to.
|
||||
func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger, pointsOnCorrect, pointsOnIncorrect int) (*scoreboard, error) {
|
||||
fileBody, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading saved scored: %w", err)
|
||||
}
|
||||
|
||||
scoresM := map[string]int{}
|
||||
if len(fileBody) > 0 {
|
||||
if err := json.Unmarshal(fileBody, &scoresM); err != nil {
|
||||
return nil, fmt.Errorf("decoding saved scores: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scoreboard := &scoreboard{
|
||||
file: file,
|
||||
scoresM: scoresM,
|
||||
pointsOnCorrect: pointsOnCorrect,
|
||||
pointsOnIncorrect: pointsOnIncorrect,
|
||||
cleanupCh: make(chan struct{}),
|
||||
saveLoopWaitCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
scoreboard.cleanupWG.Add(1)
|
||||
go func() {
|
||||
scoreboard.saveLoop(saveTicker, logger)
|
||||
scoreboard.cleanupWG.Done()
|
||||
}()
|
||||
|
||||
return scoreboard, nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) cleanup() error {
|
||||
close(s.cleanupCh)
|
||||
s.cleanupWG.Wait()
|
||||
|
||||
if err := s.save(); err != nil {
|
||||
return fmt.Errorf("saving scores during cleanup: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) guessedCorrect(name string) int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
s.scoresM[name] += s.pointsOnCorrect
|
||||
return s.scoresM[name]
|
||||
}
|
||||
|
||||
func (s *scoreboard) guessedIncorrect(name string) int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
s.scoresM[name] += s.pointsOnIncorrect
|
||||
return s.scoresM[name]
|
||||
}
|
||||
|
||||
func (s *scoreboard) scores() map[string]int {
|
||||
s.scoresLock.Lock()
|
||||
defer s.scoresLock.Unlock()
|
||||
|
||||
scoresCp := map[string]int{}
|
||||
for name, score := range s.scoresM {
|
||||
scoresCp[name] = score
|
||||
}
|
||||
|
||||
return scoresCp
|
||||
}
|
||||
|
||||
func (s *scoreboard) save() error {
|
||||
scores := s.scores()
|
||||
if _, err := s.file.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("seeking to start of save file: %w", err)
|
||||
} else if err := s.file.Truncate(0); err != nil {
|
||||
return fmt.Errorf("truncating save file: %w", err)
|
||||
} else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
|
||||
return fmt.Errorf("encoding scores to save file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
if err := s.save(); err != nil {
|
||||
logger.Printf("error saving scoreboard to file: %v", err)
|
||||
}
|
||||
case <-s.cleanupCh:
|
||||
return
|
||||
case <-s.saveLoopWaitCh:
|
||||
// test will unblock, nothing to do here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The httpHandlers component
|
||||
|
||||
// Scoreboard describes the scoreboard component from the point of view of the
|
||||
// httpHandler component (which only needs a subset of scoreboard's methods).
|
||||
type Scoreboard interface {
|
||||
guessedCorrect(name string) int
|
||||
guessedIncorrect(name string) int
|
||||
scores() map[string]int
|
||||
}
|
||||
|
||||
// RandSrc describes a randomness component which can produce random integers.
|
||||
type RandSrc interface {
|
||||
Int() int
|
||||
}
|
||||
|
||||
// httpHandlers implements the http.HandlerFuncs used by the httpServer.
|
||||
type httpHandlers struct {
|
||||
scoreboard Scoreboard
|
||||
randSrc RandSrc
|
||||
logger Logger
|
||||
|
||||
mux *http.ServeMux
|
||||
n int
|
||||
nLock sync.Mutex
|
||||
}
|
||||
|
||||
func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
|
||||
n := randSrc.Int()
|
||||
logger.Printf("first n is %v", n)
|
||||
|
||||
httpHandlers := &httpHandlers{
|
||||
scoreboard: scoreboard,
|
||||
randSrc: randSrc,
|
||||
logger: logger,
|
||||
mux: http.NewServeMux(),
|
||||
n: n,
|
||||
}
|
||||
|
||||
httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
|
||||
httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
|
||||
|
||||
return httpHandlers
|
||||
}
|
||||
|
||||
func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
name := r.FormValue("name")
|
||||
nStr := r.FormValue("n")
|
||||
if name == "" || nStr == "" {
|
||||
http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(nStr)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.nLock.Lock()
|
||||
defer h.nLock.Unlock()
|
||||
|
||||
if h.n == n {
|
||||
newScore := h.scoreboard.guessedCorrect(name)
|
||||
h.n = h.randSrc.Int()
|
||||
h.logger.Printf("new n is %v", h.n)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
|
||||
return
|
||||
}
|
||||
|
||||
hint := "higher"
|
||||
if h.n < n {
|
||||
hint = "lower"
|
||||
}
|
||||
|
||||
newScore := h.scoreboard.guessedIncorrect(name)
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
|
||||
}
|
||||
|
||||
func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "text/plain")
|
||||
|
||||
h.nLock.Lock()
|
||||
defer h.nLock.Unlock()
|
||||
|
||||
type scoreTup struct {
|
||||
name string
|
||||
score int
|
||||
}
|
||||
|
||||
scores := h.scoreboard.scores()
|
||||
scoresTups := make([]scoreTup, 0, len(scores))
|
||||
for name, score := range scores {
|
||||
scoresTups = append(scoresTups, scoreTup{name, score})
|
||||
}
|
||||
|
||||
sort.Slice(scoresTups, func(i, j int) bool {
|
||||
return scoresTups[i].score > scoresTups[j].score
|
||||
})
|
||||
|
||||
for _, scoresTup := range scoresTups {
|
||||
fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The httpServer component.
|
||||
|
||||
type httpServer struct {
|
||||
httpServer *http.Server
|
||||
errCh chan error
|
||||
}
|
||||
|
||||
func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
|
||||
loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
|
||||
httpHandlers.ServeHTTP(rw, r)
|
||||
})
|
||||
|
||||
server := &httpServer{
|
||||
httpServer: &http.Server{
|
||||
Handler: loggingHandler,
|
||||
},
|
||||
errCh: make(chan error, 1),
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.httpServer.Serve(listener)
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
err = nil
|
||||
}
|
||||
server.errCh <- err
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *httpServer) cleanup() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("shutting down http server: %w", err)
|
||||
}
|
||||
return <-s.errCh
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// main
|
||||
|
||||
func main() {
|
||||
saveFilePath := flag.String("save-file", "./save.json", "File used to save scores")
|
||||
listenAddr := flag.String("listen-addr", ":8888", "Address to listen for HTTP requests on")
|
||||
saveInterval := flag.Duration("save-interval", 5*time.Second, "How often to resave scores")
|
||||
pointsOnCorrect := flag.Int("points-on-correct", 1000, "Amount to change a user's score by upon a correct score")
|
||||
pointsOnIncorrect := flag.Int("points-on-incorrect", -1, "Amount to change a user's score by upon an incorrect score")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||
|
||||
logger.Printf("opening scoreboard save file %q", *saveFilePath)
|
||||
file, err := os.OpenFile(*saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to open file %q: %v", *saveFilePath, err)
|
||||
}
|
||||
|
||||
saveTicker := time.NewTicker(*saveInterval)
|
||||
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
logger.Printf("initializing scoreboard")
|
||||
scoreboard, err := newScoreboard(file, saveTicker.C, logger, *pointsOnCorrect, *pointsOnIncorrect)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to initialize scoreboard: %v", err)
|
||||
}
|
||||
|
||||
logger.Printf("listening on %q", *listenAddr)
|
||||
listener, err := net.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to listen on %q: %v", *listenAddr, err)
|
||||
}
|
||||
|
||||
logger.Printf("setting up HTTP handlers")
|
||||
httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
|
||||
|
||||
logger.Printf("serving HTTP requests")
|
||||
httpServer := newHTTPServer(listener, httpHandlers, logger)
|
||||
|
||||
logger.Printf("initialization done, waiting for interrupt signal")
|
||||
sigCh := make(chan os.Signal)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
<-sigCh
|
||||
logger.Printf("interrupt signal received, cleaning up")
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Fatalf("interrupt signal received again, forcing shutdown")
|
||||
}()
|
||||
|
||||
if err := httpServer.cleanup(); err != nil {
|
||||
logger.Fatalf("cleaning up http server: %v", err)
|
||||
}
|
||||
|
||||
// NOTE go's builtin http server does not follow component property 5a, and
|
||||
// instead closes the net.Listener given to it as a parameter when Shutdown
|
||||
// is called. Because of that inconsistency this Close would error if it
|
||||
// were called.
|
||||
//
|
||||
// While there are ways to work around this, it's instead highlighted in
|
||||
// this example as an instance of a language making the component-oriented
|
||||
// pattern more difficult.
|
||||
//
|
||||
//if err := listener.Close(); err != nil {
|
||||
// logger.Fatalf("closing listener %q: %v", listenAddr, err)
|
||||
//}
|
||||
|
||||
if err := scoreboard.cleanup(); err != nil {
|
||||
logger.Fatalf("cleaning up scoreboard: %v", err)
|
||||
}
|
||||
|
||||
saveTicker.Stop()
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
logger.Fatalf("closing file %q: %v", *saveFilePath, err)
|
||||
}
|
||||
|
||||
os.Stdout.Sync()
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
bundle:
|
||||
nix-build markov.nix -A nixBundle
|
||||
./result/bin/nix-bundle '((import ./markov.nix) {}).entrypoint' '/bin/markov'
|
@ -1,63 +0,0 @@
|
||||
{
|
||||
pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/d50923ab2d308a1ddb21594ba6ae064cab65d8ae.tar.gz") {}
|
||||
}:
|
||||
|
||||
rec {
|
||||
|
||||
markov = pkgs.buildGoModule {
|
||||
pname = "markov";
|
||||
version = "618b666484566de71f2d59114d011ff4621cf375";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "mediocregopher";
|
||||
repo = "markov";
|
||||
rev = "618b666484566de71f2d59114d011ff4621cf375";
|
||||
sha256 = "1sx9dr1q3vr3q8nyx3965x6259iyl85591vx815g1xacygv4i4fg";
|
||||
};
|
||||
vendorSha256 = "048wygrmv26fsnypsp6vxf89z3j0gs9f1w4i63khx7h134yxhbc6";
|
||||
};
|
||||
|
||||
entrypoint = pkgs.writeScriptBin "markov" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
|
||||
# On every run we create new, temporary, configuration files for redis and
|
||||
# circus. To do this we create a new config directory.
|
||||
markovCfgDir=$(${pkgs.coreutils}/bin/mktemp -d)
|
||||
echo "generating configuration to $markovCfgDir"
|
||||
|
||||
${pkgs.coreutils}/bin/cat >$markovCfgDir/redis.conf <<EOF
|
||||
save ""
|
||||
dir "''${MARKOV_DATA_DIR:-$(pwd)}"
|
||||
appendonly yes
|
||||
appendfilename "markov.data"
|
||||
EOF
|
||||
|
||||
${pkgs.coreutils}/bin/cat >$markovCfgDir/circus.ini <<EOF
|
||||
|
||||
[circus]
|
||||
|
||||
[watcher:markov]
|
||||
cmd = ${markov}/bin/markov \
|
||||
-listenAddr ''${MARKOV_LISTEN_ADDR:-localhost:8000} \
|
||||
-timeout ''${MARKOV_TIMEOUT:-720}
|
||||
numprocesses = 1
|
||||
|
||||
[watcher:redis]
|
||||
cmd = ${pkgs.redis}/bin/redis-server $markovCfgDir/redis.conf
|
||||
numprocesses = 1
|
||||
EOF
|
||||
|
||||
exec ${pkgs.circus}/bin/circusd $markovCfgDir/circus.ini
|
||||
'';
|
||||
|
||||
nixBundleSrc = pkgs.fetchFromGitHub {
|
||||
owner = "matthewbauer";
|
||||
repo = "nix-bundle";
|
||||
rev = "8e396533ef8f3e8a769037476824d668409b4a74";
|
||||
sha256 = "1lrq0990p07av42xz203w64abv2rz9xd8jrzxyvzzwj7vjj7qwyw";
|
||||
};
|
||||
|
||||
nixBundle = (import "${nixBundleSrc}/release.nix") {
|
||||
nixpkgs' = pkgs;
|
||||
};
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
function CW(resource) {
|
||||
this.conn = new WebSocket('wss://stream.cryptowat.ch/connect?apikey=GPDLXH702E1NAD96OSBO');
|
||||
this.conn.binaryType = "arraybuffer";
|
||||
|
||||
this.conn.onopen = () => {
|
||||
console.log("CW websocket connected");
|
||||
if (this.onconnect) this.onconnect();
|
||||
}
|
||||
|
||||
let decoder = new TextDecoder();
|
||||
this.conn.onmessage = (msg) => {
|
||||
let d = JSON.parse(decoder.decode(msg.data));
|
||||
|
||||
// The server will always send an AUTHENTICATED signal when you establish a valid connection
|
||||
// At this point you can subscribe to resources
|
||||
if (d.authenticationResult && d.authenticationResult.status === 'AUTHENTICATED') {
|
||||
if (this.onauth) this.onauth();
|
||||
this.conn.send(JSON.stringify({
|
||||
subscribe: {
|
||||
subscriptions: [
|
||||
{streamSubscription: {resource: resource}},
|
||||
],
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Market data comes in a marketUpdate
|
||||
// In this case, we're expecting trades so we look for marketUpdate.tradesUpdate
|
||||
if (!d.marketUpdate || !d.marketUpdate.tradesUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
let trades = d.marketUpdate.tradesUpdate.trades;
|
||||
for (let i in trades) {
|
||||
trades[i].price = parseFloat(trades[i].priceStr);
|
||||
trades[i].volume = parseFloat(trades[i].amountStr);
|
||||
}
|
||||
if (this.ontrades) this.ontrades(trades);
|
||||
}
|
||||
|
||||
this.close = () => this.conn.close();
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
function distribute(val, minOld, maxOld, minNew, maxNew) {
|
||||
let scalar = (val - minOld) / (maxOld - minOld);
|
||||
return minNew + ((maxNew - minNew) * scalar);
|
||||
}
|
||||
|
||||
function Distributor(capacity) {
|
||||
this.cap = capacity;
|
||||
|
||||
this.reset = () => {
|
||||
this.arr = [];
|
||||
this.arrSorted = [];
|
||||
this.length = 0;
|
||||
};
|
||||
this.reset();
|
||||
|
||||
// add adds the given value into the series, shifting off the oldest value if
|
||||
// the series is at capacity.
|
||||
this.add = (val) => {
|
||||
this.arr.push(val);
|
||||
if (this.arr.length >= this.cap) this.arr.shift();
|
||||
this.arrSorted = this.arr.slice(); // copy array
|
||||
this.arrSorted.sort();
|
||||
this.length = this.arr.length;
|
||||
};
|
||||
|
||||
// distribute finds where the given value falls within the series, and then
|
||||
// scales that into the given range (inclusive).
|
||||
this.distribute = (val, min, max) => {
|
||||
if (this.length == 0) throw "cannot locate within empty Distributor";
|
||||
|
||||
let idx = this.length;
|
||||
for (i in this.arrSorted) {
|
||||
if (val < this.arrSorted[i]) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return distribute(idx, 0, this.length, min, max);
|
||||
};
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2013 MIDI.js Authors. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -1,61 +0,0 @@
|
||||
//https://github.com/davidchambers/Base64.js
|
||||
|
||||
;(function () {
|
||||
var object = typeof exports != 'undefined' ? exports : this; // #8: web workers
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
|
||||
function InvalidCharacterError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
InvalidCharacterError.prototype = new Error;
|
||||
InvalidCharacterError.prototype.name = 'InvalidCharacterError';
|
||||
|
||||
// encoder
|
||||
// [https://gist.github.com/999166] by [https://github.com/nignag]
|
||||
object.btoa || (
|
||||
object.btoa = function (input) {
|
||||
for (
|
||||
// initialize result and counter
|
||||
var block, charCode, idx = 0, map = chars, output = '';
|
||||
// if the next input index does not exist:
|
||||
// change the mapping table to "="
|
||||
// check if d has no fractional digits
|
||||
input.charAt(idx | 0) || (map = '=', idx % 1);
|
||||
// "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
|
||||
output += map.charAt(63 & block >> 8 - idx % 1 * 8)
|
||||
) {
|
||||
charCode = input.charCodeAt(idx += 3/4);
|
||||
if (charCode > 0xFF) {
|
||||
throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
|
||||
}
|
||||
block = block << 8 | charCode;
|
||||
}
|
||||
return output;
|
||||
});
|
||||
|
||||
// decoder
|
||||
// [https://gist.github.com/1020396] by [https://github.com/atk]
|
||||
object.atob || (
|
||||
object.atob = function (input) {
|
||||
input = input.replace(/=+$/, '')
|
||||
if (input.length % 4 == 1) {
|
||||
throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
|
||||
}
|
||||
for (
|
||||
// initialize result and counters
|
||||
var bc = 0, bs, buffer, idx = 0, output = '';
|
||||
// get next character
|
||||
buffer = input.charAt(idx++);
|
||||
// character found in table? initialize bit storage and add its ascii value;
|
||||
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
|
||||
// and if not first of each 4 characters,
|
||||
// convert the first 8 bits to one ascii character
|
||||
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
|
||||
) {
|
||||
// try to find character in table (0-63, not found => -1)
|
||||
buffer = chars.indexOf(buffer);
|
||||
}
|
||||
return output;
|
||||
});
|
||||
|
||||
}());
|
@ -1,81 +0,0 @@
|
||||
/**
|
||||
* @license -------------------------------------------------------------------
|
||||
* module: Base64Binary
|
||||
* src: http://blog.danguer.com/2011/10/24/base64-binary-decoding-in-javascript/
|
||||
* license: Simplified BSD License
|
||||
* -------------------------------------------------------------------
|
||||
* Copyright 2011, Daniel Guerrero. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
var Base64Binary = {
|
||||
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||
|
||||
/* will return a Uint8Array type */
|
||||
decodeArrayBuffer: function(input) {
|
||||
var bytes = Math.ceil( (3*input.length) / 4.0);
|
||||
var ab = new ArrayBuffer(bytes);
|
||||
this.decode(input, ab);
|
||||
|
||||
return ab;
|
||||
},
|
||||
|
||||
decode: function(input, arrayBuffer) {
|
||||
//get last chars to see if are valid
|
||||
var lkey1 = this._keyStr.indexOf(input.charAt(input.length-1));
|
||||
var lkey2 = this._keyStr.indexOf(input.charAt(input.length-1));
|
||||
|
||||
var bytes = Math.ceil( (3*input.length) / 4.0);
|
||||
if (lkey1 == 64) bytes--; //padding chars, so skip
|
||||
if (lkey2 == 64) bytes--; //padding chars, so skip
|
||||
|
||||
var uarray;
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
|
||||
if (arrayBuffer)
|
||||
uarray = new Uint8Array(arrayBuffer);
|
||||
else
|
||||
uarray = new Uint8Array(bytes);
|
||||
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
|
||||
for (i=0; i<bytes; i+=3) {
|
||||
//get the 3 octects in 4 ascii chars
|
||||
enc1 = this._keyStr.indexOf(input.charAt(j++));
|
||||
enc2 = this._keyStr.indexOf(input.charAt(j++));
|
||||
enc3 = this._keyStr.indexOf(input.charAt(j++));
|
||||
enc4 = this._keyStr.indexOf(input.charAt(j++));
|
||||
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
|
||||
uarray[i] = chr1;
|
||||
if (enc3 != 64) uarray[i+1] = chr2;
|
||||
if (enc4 != 64) uarray[i+2] = chr3;
|
||||
}
|
||||
|
||||
return uarray;
|
||||
}
|
||||
};
|
@ -1,111 +0,0 @@
|
||||
/**
|
||||
* @license -------------------------------------------------------------------
|
||||
* module: WebAudioShim - Fix naming issues for WebAudioAPI supports
|
||||
* src: https://github.com/Dinahmoe/webaudioshim
|
||||
* author: Dinahmoe AB
|
||||
* -------------------------------------------------------------------
|
||||
* Copyright (c) 2012 DinahMoe AB
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
window.AudioContext = window.AudioContext || window.webkitAudioContext || null;
|
||||
window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext || null;
|
||||
|
||||
(function (Context) {
|
||||
var isFunction = function (f) {
|
||||
return Object.prototype.toString.call(f) === "[object Function]" ||
|
||||
Object.prototype.toString.call(f) === "[object AudioContextConstructor]";
|
||||
};
|
||||
var contextMethods = [
|
||||
["createGainNode", "createGain"],
|
||||
["createDelayNode", "createDelay"],
|
||||
["createJavaScriptNode", "createScriptProcessor"]
|
||||
];
|
||||
///
|
||||
var proto;
|
||||
var instance;
|
||||
var sourceProto;
|
||||
///
|
||||
if (!isFunction(Context)) {
|
||||
return;
|
||||
}
|
||||
instance = new Context();
|
||||
if (!instance.destination || !instance.sampleRate) {
|
||||
return;
|
||||
}
|
||||
proto = Context.prototype;
|
||||
sourceProto = Object.getPrototypeOf(instance.createBufferSource());
|
||||
|
||||
if (!isFunction(sourceProto.start)) {
|
||||
if (isFunction(sourceProto.noteOn)) {
|
||||
sourceProto.start = function (when, offset, duration) {
|
||||
switch (arguments.length) {
|
||||
case 0:
|
||||
throw new Error("Not enough arguments.");
|
||||
case 1:
|
||||
this.noteOn(when);
|
||||
break;
|
||||
case 2:
|
||||
if (this.buffer) {
|
||||
this.noteGrainOn(when, offset, this.buffer.duration - offset);
|
||||
} else {
|
||||
throw new Error("Missing AudioBuffer");
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
this.noteGrainOn(when, offset, duration);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFunction(sourceProto.noteOn)) {
|
||||
sourceProto.noteOn = sourceProto.start;
|
||||
}
|
||||
|
||||
if (!isFunction(sourceProto.noteGrainOn)) {
|
||||
sourceProto.noteGrainOn = sourceProto.start;
|
||||
}
|
||||
|
||||
if (!isFunction(sourceProto.stop)) {
|
||||
sourceProto.stop = sourceProto.noteOff;
|
||||
}
|
||||
|
||||
if (!isFunction(sourceProto.noteOff)) {
|
||||
sourceProto.noteOff = sourceProto.stop;
|
||||
}
|
||||
|
||||
contextMethods.forEach(function (names) {
|
||||
var name1;
|
||||
var name2;
|
||||
while (names.length) {
|
||||
name1 = names.pop();
|
||||
if (isFunction(this[name1])) {
|
||||
this[names.pop()] = this[name1];
|
||||
} else {
|
||||
name2 = names.pop();
|
||||
this[name1] = this[name2];
|
||||
}
|
||||
}
|
||||
}, proto);
|
||||
})(window.AudioContext);
|
@ -1,421 +0,0 @@
|
||||
/* Copyright 2013 Chris Wilson
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Initialize the MIDI library.
|
||||
(function (global) {
|
||||
'use strict';
|
||||
var midiIO, _requestMIDIAccess, MIDIAccess, _onReady, MIDIPort, MIDIInput, MIDIOutput, _midiProc;
|
||||
|
||||
function Promise() {
|
||||
|
||||
}
|
||||
|
||||
Promise.prototype.then = function(accept, reject) {
|
||||
this.accept = accept;
|
||||
this.reject = reject;
|
||||
}
|
||||
|
||||
Promise.prototype.succeed = function(access) {
|
||||
if (this.accept)
|
||||
this.accept(access);
|
||||
}
|
||||
|
||||
Promise.prototype.fail = function(error) {
|
||||
if (this.reject)
|
||||
this.reject(error);
|
||||
}
|
||||
|
||||
function _JazzInstance() {
|
||||
this.inputInUse = false;
|
||||
this.outputInUse = false;
|
||||
|
||||
// load the Jazz plugin
|
||||
var o1 = document.createElement("object");
|
||||
o1.id = "_Jazz" + Math.random() + "ie";
|
||||
o1.classid = "CLSID:1ACE1618-1C7D-4561-AEE1-34842AA85E90";
|
||||
|
||||
this.activeX = o1;
|
||||
|
||||
var o2 = document.createElement("object");
|
||||
o2.id = "_Jazz" + Math.random();
|
||||
o2.type="audio/x-jazz";
|
||||
o1.appendChild(o2);
|
||||
|
||||
this.objRef = o2;
|
||||
|
||||
var e = document.createElement("p");
|
||||
e.appendChild(document.createTextNode("This page requires the "));
|
||||
|
||||
var a = document.createElement("a");
|
||||
a.appendChild(document.createTextNode("Jazz plugin"));
|
||||
a.href = "http://jazz-soft.net/";
|
||||
|
||||
e.appendChild(a);
|
||||
e.appendChild(document.createTextNode("."));
|
||||
o2.appendChild(e);
|
||||
|
||||
var insertionPoint = document.getElementById("MIDIPlugin");
|
||||
if (!insertionPoint) {
|
||||
// Create hidden element
|
||||
var insertionPoint = document.createElement("div");
|
||||
insertionPoint.id = "MIDIPlugin";
|
||||
insertionPoint.style.position = "absolute";
|
||||
insertionPoint.style.visibility = "hidden";
|
||||
insertionPoint.style.left = "-9999px";
|
||||
insertionPoint.style.top = "-9999px";
|
||||
document.body.appendChild(insertionPoint);
|
||||
}
|
||||
insertionPoint.appendChild(o1);
|
||||
|
||||
if (this.objRef.isJazz)
|
||||
this._Jazz = this.objRef;
|
||||
else if (this.activeX.isJazz)
|
||||
this._Jazz = this.activeX;
|
||||
else
|
||||
this._Jazz = null;
|
||||
if (this._Jazz) {
|
||||
this._Jazz._jazzTimeZero = this._Jazz.Time();
|
||||
this._Jazz._perfTimeZero = window.performance.now();
|
||||
}
|
||||
}
|
||||
|
||||
_requestMIDIAccess = function _requestMIDIAccess() {
|
||||
var access = new MIDIAccess();
|
||||
return access._promise;
|
||||
};
|
||||
|
||||
// API Methods
|
||||
|
||||
MIDIAccess = function() {
|
||||
this._jazzInstances = new Array();
|
||||
this._jazzInstances.push( new _JazzInstance() );
|
||||
this._promise = new Promise;
|
||||
|
||||
if (this._jazzInstances[0]._Jazz) {
|
||||
this._Jazz = this._jazzInstances[0]._Jazz;
|
||||
window.setTimeout( _onReady.bind(this), 3 );
|
||||
} else {
|
||||
window.setTimeout( _onNotReady.bind(this), 3 );
|
||||
}
|
||||
};
|
||||
|
||||
_onReady = function _onReady() {
|
||||
if (this._promise)
|
||||
this._promise.succeed(this);
|
||||
};
|
||||
|
||||
function _onNotReady() {
|
||||
if (this._promise)
|
||||
this._promise.fail( { code: 1 } );
|
||||
};
|
||||
|
||||
MIDIAccess.prototype.inputs = function( ) {
|
||||
if (!this._Jazz)
|
||||
return null;
|
||||
var list=this._Jazz.MidiInList();
|
||||
var inputs = new Array( list.length );
|
||||
|
||||
for ( var i=0; i<list.length; i++ ) {
|
||||
inputs[i] = new MIDIInput( this, list[i], i );
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
MIDIAccess.prototype.outputs = function( ) {
|
||||
if (!this._Jazz)
|
||||
return null;
|
||||
var list=this._Jazz.MidiOutList();
|
||||
var outputs = new Array( list.length );
|
||||
|
||||
for ( var i=0; i<list.length; i++ ) {
|
||||
outputs[i] = new MIDIOutput( this, list[i], i );
|
||||
}
|
||||
return outputs;
|
||||
};
|
||||
|
||||
MIDIInput = function MIDIInput( midiAccess, name, index ) {
|
||||
this._listeners = [];
|
||||
this._midiAccess = midiAccess;
|
||||
this._index = index;
|
||||
this._inLongSysexMessage = false;
|
||||
this._sysexBuffer = new Uint8Array();
|
||||
this.id = "" + index + "." + name;
|
||||
this.manufacturer = "";
|
||||
this.name = name;
|
||||
this.type = "input";
|
||||
this.version = "";
|
||||
this.onmidimessage = null;
|
||||
|
||||
var inputInstance = null;
|
||||
for (var i=0; (i<midiAccess._jazzInstances.length)&&(!inputInstance); i++) {
|
||||
if (!midiAccess._jazzInstances[i].inputInUse)
|
||||
inputInstance=midiAccess._jazzInstances[i];
|
||||
}
|
||||
if (!inputInstance) {
|
||||
inputInstance = new _JazzInstance();
|
||||
midiAccess._jazzInstances.push( inputInstance );
|
||||
}
|
||||
inputInstance.inputInUse = true;
|
||||
|
||||
this._jazzInstance = inputInstance._Jazz;
|
||||
this._input = this._jazzInstance.MidiInOpen( this._index, _midiProc.bind(this) );
|
||||
};
|
||||
|
||||
// Introduced in DOM Level 2:
|
||||
MIDIInput.prototype.addEventListener = function (type, listener, useCapture ) {
|
||||
if (type !== "midimessage")
|
||||
return;
|
||||
for (var i=0; i<this._listeners.length; i++)
|
||||
if (this._listeners[i] == listener)
|
||||
return;
|
||||
this._listeners.push( listener );
|
||||
};
|
||||
|
||||
MIDIInput.prototype.removeEventListener = function (type, listener, useCapture ) {
|
||||
if (type !== "midimessage")
|
||||
return;
|
||||
for (var i=0; i<this._listeners.length; i++)
|
||||
if (this._listeners[i] == listener) {
|
||||
this._listeners.splice( i, 1 ); //remove it
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
MIDIInput.prototype.preventDefault = function() {
|
||||
this._pvtDef = true;
|
||||
};
|
||||
|
||||
MIDIInput.prototype.dispatchEvent = function (evt) {
|
||||
this._pvtDef = false;
|
||||
|
||||
// dispatch to listeners
|
||||
for (var i=0; i<this._listeners.length; i++)
|
||||
if (this._listeners[i].handleEvent)
|
||||
this._listeners[i].handleEvent.bind(this)( evt );
|
||||
else
|
||||
this._listeners[i].bind(this)( evt );
|
||||
|
||||
if (this.onmidimessage)
|
||||
this.onmidimessage( evt );
|
||||
|
||||
return this._pvtDef;
|
||||
};
|
||||
|
||||
MIDIInput.prototype.appendToSysexBuffer = function ( data ) {
|
||||
var oldLength = this._sysexBuffer.length;
|
||||
var tmpBuffer = new Uint8Array( oldLength + data.length );
|
||||
tmpBuffer.set( this._sysexBuffer );
|
||||
tmpBuffer.set( data, oldLength );
|
||||
this._sysexBuffer = tmpBuffer;
|
||||
};
|
||||
|
||||
MIDIInput.prototype.bufferLongSysex = function ( data, initialOffset ) {
|
||||
var j = initialOffset;
|
||||
while (j<data.length) {
|
||||
if (data[j] == 0xF7) {
|
||||
// end of sysex!
|
||||
j++;
|
||||
this.appendToSysexBuffer( data.slice(initialOffset, j) );
|
||||
return j;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
// didn't reach the end; just tack it on.
|
||||
this.appendToSysexBuffer( data.slice(initialOffset, j) );
|
||||
this._inLongSysexMessage = true;
|
||||
return j;
|
||||
};
|
||||
|
||||
_midiProc = function _midiProc( timestamp, data ) {
|
||||
// Have to use createEvent/initEvent because IE10 fails on new CustomEvent. Thanks, IE!
|
||||
var length = 0;
|
||||
var i,j;
|
||||
var isSysexMessage = false;
|
||||
|
||||
// Jazz sometimes passes us multiple messages at once, so we need to parse them out
|
||||
// and pass them one at a time.
|
||||
|
||||
for (i=0; i<data.length; i+=length) {
|
||||
if (this._inLongSysexMessage) {
|
||||
i = this.bufferLongSysex(data,i);
|
||||
if ( data[i-1] != 0xf7 ) {
|
||||
// ran off the end without hitting the end of the sysex message
|
||||
return;
|
||||
}
|
||||
isSysexMessage = true;
|
||||
} else {
|
||||
isSysexMessage = false;
|
||||
switch (data[i] & 0xF0) {
|
||||
case 0x80: // note off
|
||||
case 0x90: // note on
|
||||
case 0xA0: // polyphonic aftertouch
|
||||
case 0xB0: // control change
|
||||
case 0xE0: // channel mode
|
||||
length = 3;
|
||||
break;
|
||||
|
||||
case 0xC0: // program change
|
||||
case 0xD0: // channel aftertouch
|
||||
length = 2;
|
||||
break;
|
||||
|
||||
case 0xF0:
|
||||
switch (data[i]) {
|
||||
case 0xf0: // variable-length sysex.
|
||||
i = this.bufferLongSysex(data,i);
|
||||
if ( data[i-1] != 0xf7 ) {
|
||||
// ran off the end without hitting the end of the sysex message
|
||||
return;
|
||||
}
|
||||
isSysexMessage = true;
|
||||
break;
|
||||
|
||||
case 0xF1: // MTC quarter frame
|
||||
case 0xF3: // song select
|
||||
length = 2;
|
||||
break;
|
||||
|
||||
case 0xF2: // song position pointer
|
||||
length = 3;
|
||||
break;
|
||||
|
||||
default:
|
||||
length = 1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
var evt = document.createEvent( "Event" );
|
||||
evt.initEvent( "midimessage", false, false );
|
||||
evt.receivedTime = parseFloat( timestamp.toString()) + this._jazzInstance._perfTimeZero;
|
||||
if (isSysexMessage || this._inLongSysexMessage) {
|
||||
evt.data = new Uint8Array( this._sysexBuffer );
|
||||
this._sysexBuffer = new Uint8Array(0);
|
||||
this._inLongSysexMessage = false;
|
||||
} else
|
||||
evt.data = new Uint8Array(data.slice(i, length+i));
|
||||
this.dispatchEvent( evt );
|
||||
}
|
||||
};
|
||||
|
||||
MIDIOutput = function MIDIOutput( midiAccess, name, index ) {
|
||||
this._listeners = [];
|
||||
this._midiAccess = midiAccess;
|
||||
this._index = index;
|
||||
this.id = "" + index + "." + name;
|
||||
this.manufacturer = "";
|
||||
this.name = name;
|
||||
this.type = "output";
|
||||
this.version = "";
|
||||
|
||||
var outputInstance = null;
|
||||
for (var i=0; (i<midiAccess._jazzInstances.length)&&(!outputInstance); i++) {
|
||||
if (!midiAccess._jazzInstances[i].outputInUse)
|
||||
outputInstance=midiAccess._jazzInstances[i];
|
||||
}
|
||||
if (!outputInstance) {
|
||||
outputInstance = new _JazzInstance();
|
||||
midiAccess._jazzInstances.push( outputInstance );
|
||||
}
|
||||
outputInstance.outputInUse = true;
|
||||
|
||||
this._jazzInstance = outputInstance._Jazz;
|
||||
this._jazzInstance.MidiOutOpen(this.name);
|
||||
};
|
||||
|
||||
function _sendLater() {
|
||||
this.jazz.MidiOutLong( this.data ); // handle send as sysex
|
||||
}
|
||||
|
||||
MIDIOutput.prototype.send = function( data, timestamp ) {
|
||||
var delayBeforeSend = 0;
|
||||
if (data.length === 0)
|
||||
return false;
|
||||
|
||||
if (timestamp)
|
||||
delayBeforeSend = Math.floor( timestamp - window.performance.now() );
|
||||
|
||||
if (timestamp && (delayBeforeSend>1)) {
|
||||
var sendObj = new Object();
|
||||
sendObj.jazz = this._jazzInstance;
|
||||
sendObj.data = data;
|
||||
|
||||
window.setTimeout( _sendLater.bind(sendObj), delayBeforeSend );
|
||||
} else {
|
||||
this._jazzInstance.MidiOutLong( data );
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
//init: create plugin
|
||||
if (!window.navigator.requestMIDIAccess)
|
||||
window.navigator.requestMIDIAccess = _requestMIDIAccess;
|
||||
|
||||
}(window));
|
||||
|
||||
// Polyfill window.performance.now() if necessary.
|
||||
(function (exports) {
|
||||
var perf = {}, props;
|
||||
|
||||
function findAlt() {
|
||||
var prefix = ['moz', 'webkit', 'o', 'ms'],
|
||||
i = prefix.length,
|
||||
//worst case, we use Date.now()
|
||||
props = {
|
||||
value: (function (start) {
|
||||
return function () {
|
||||
return Date.now() - start;
|
||||
};
|
||||
}(Date.now()))
|
||||
};
|
||||
|
||||
//seach for vendor prefixed version
|
||||
for (; i >= 0; i--) {
|
||||
if ((prefix[i] + "Now") in exports.performance) {
|
||||
props.value = function (method) {
|
||||
return function () {
|
||||
exports.performance[method]();
|
||||
}
|
||||
}(prefix[i] + "Now");
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
//otherwise, try to use connectionStart
|
||||
if ("timing" in exports.performance && "connectStart" in exports.performance.timing) {
|
||||
//this pretty much approximates performance.now() to the millisecond
|
||||
props.value = (function (start) {
|
||||
return function() {
|
||||
Date.now() - start;
|
||||
};
|
||||
}(exports.performance.timing.connectStart));
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
//if already defined, bail
|
||||
if (("performance" in exports) && ("now" in exports.performance))
|
||||
return;
|
||||
if (!("performance" in exports))
|
||||
Object.defineProperty(exports, "performance", {
|
||||
get: function () {
|
||||
return perf;
|
||||
}});
|
||||
//otherwise, performance is there, but not "now()"
|
||||
|
||||
props = findAlt();
|
||||
Object.defineProperty(exports.performance, "now", props);
|
||||
}(window));
|
@ -1,101 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
MIDI.audioDetect : 0.3.2 : 2015-03-26
|
||||
----------------------------------------------------------
|
||||
https://github.com/mudcube/MIDI.js
|
||||
----------------------------------------------------------
|
||||
Probably, Maybe, No... Absolutely!
|
||||
Test to see what types of <audio> MIME types are playable by the browser.
|
||||
----------------------------------------------------------
|
||||
*/
|
||||
|
||||
if (typeof MIDI === 'undefined') MIDI = {};
|
||||
|
||||
(function(root) { 'use strict';
|
||||
|
||||
var supports = {}; // object of supported file types
|
||||
var pending = 0; // pending file types to process
|
||||
var canPlayThrough = function (src) { // check whether format plays through
|
||||
pending ++;
|
||||
var body = document.body;
|
||||
var audio = new Audio();
|
||||
var mime = src.split(';')[0];
|
||||
audio.id = 'audio';
|
||||
audio.setAttribute('preload', 'auto');
|
||||
audio.setAttribute('audiobuffer', true);
|
||||
audio.addEventListener('error', function() {
|
||||
body.removeChild(audio);
|
||||
supports[mime] = false;
|
||||
pending --;
|
||||
}, false);
|
||||
audio.addEventListener('canplaythrough', function() {
|
||||
body.removeChild(audio);
|
||||
supports[mime] = true;
|
||||
pending --;
|
||||
}, false);
|
||||
audio.src = 'data:' + src;
|
||||
body.appendChild(audio);
|
||||
};
|
||||
|
||||
root.audioDetect = function(onsuccess) {
|
||||
/// detect jazz-midi plugin
|
||||
if (navigator.requestMIDIAccess) {
|
||||
var isNative = Function.prototype.toString.call(navigator.requestMIDIAccess).indexOf('[native code]');
|
||||
if (isNative) { // has native midiapi support
|
||||
supports['webmidi'] = true;
|
||||
} else { // check for jazz plugin midiapi support
|
||||
for (var n = 0; navigator.plugins.length > n; n ++) {
|
||||
var plugin = navigator.plugins[n];
|
||||
if (plugin.name.indexOf('Jazz-Plugin') >= 0) {
|
||||
supports['webmidi'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// check whether <audio> tag is supported
|
||||
if (typeof(Audio) === 'undefined') {
|
||||
return onsuccess({});
|
||||
} else {
|
||||
supports['audiotag'] = true;
|
||||
}
|
||||
|
||||
/// check for webaudio api support
|
||||
if (window.AudioContext || window.webkitAudioContext) {
|
||||
supports['webaudio'] = true;
|
||||
}
|
||||
|
||||
/// check whether canPlayType is supported
|
||||
var audio = new Audio();
|
||||
if (typeof(audio.canPlayType) === 'undefined') {
|
||||
return onsuccess(supports);
|
||||
}
|
||||
|
||||
/// see what we can learn from the browser
|
||||
var vorbis = audio.canPlayType('audio/ogg; codecs="vorbis"');
|
||||
vorbis = (vorbis === 'probably' || vorbis === 'maybe');
|
||||
var mpeg = audio.canPlayType('audio/mpeg');
|
||||
mpeg = (mpeg === 'probably' || mpeg === 'maybe');
|
||||
// maybe nothing is supported
|
||||
if (!vorbis && !mpeg) {
|
||||
onsuccess(supports);
|
||||
return;
|
||||
}
|
||||
|
||||
/// or maybe something is supported
|
||||
if (vorbis) canPlayThrough('audio/ogg;base64,T2dnUwACAAAAAAAAAADqnjMlAAAAAOyyzPIBHgF2b3JiaXMAAAAAAUAfAABAHwAAQB8AAEAfAACZAU9nZ1MAAAAAAAAAAAAA6p4zJQEAAAANJGeqCj3//////////5ADdm9yYmlzLQAAAFhpcGguT3JnIGxpYlZvcmJpcyBJIDIwMTAxMTAxIChTY2hhdWZlbnVnZ2V0KQAAAAABBXZvcmJpcw9CQ1YBAAABAAxSFCElGVNKYwiVUlIpBR1jUFtHHWPUOUYhZBBTiEkZpXtPKpVYSsgRUlgpRR1TTFNJlVKWKUUdYxRTSCFT1jFloXMUS4ZJCSVsTa50FkvomWOWMUYdY85aSp1j1jFFHWNSUkmhcxg6ZiVkFDpGxehifDA6laJCKL7H3lLpLYWKW4q91xpT6y2EGEtpwQhhc+211dxKasUYY4wxxsXiUyiC0JBVAAABAABABAFCQ1YBAAoAAMJQDEVRgNCQVQBABgCAABRFcRTHcRxHkiTLAkJDVgEAQAAAAgAAKI7hKJIjSZJkWZZlWZameZaouaov+64u667t6roOhIasBACAAAAYRqF1TCqDEEPKQ4QUY9AzoxBDDEzGHGNONKQMMogzxZAyiFssLqgQBKEhKwKAKAAAwBjEGGIMOeekZFIi55iUTkoDnaPUUcoolRRLjBmlEluJMYLOUeooZZRCjKXFjFKJscRUAABAgAMAQICFUGjIigAgCgCAMAYphZRCjCnmFHOIMeUcgwwxxiBkzinoGJNOSuWck85JiRhjzjEHlXNOSuekctBJyaQTAAAQ4AAAEGAhFBqyIgCIEwAwSJKmWZomipamiaJniqrqiaKqWp5nmp5pqqpnmqpqqqrrmqrqypbnmaZnmqrqmaaqiqbquqaquq6nqrZsuqoum65q267s+rZru77uqapsm6or66bqyrrqyrbuurbtS56nqqKquq5nqq6ruq5uq65r25pqyq6purJtuq4tu7Js664s67pmqq5suqotm64s667s2rYqy7ovuq5uq7Ks+6os+75s67ru2rrwi65r66os674qy74x27bwy7ouHJMnqqqnqq7rmarrqq5r26rr2rqmmq5suq4tm6or26os67Yry7aumaosm64r26bryrIqy77vyrJui67r66Ys67oqy8Lu6roxzLat+6Lr6roqy7qvyrKuu7ru+7JuC7umqrpuyrKvm7Ks+7auC8us27oxuq7vq7It/KosC7+u+8Iy6z5jdF1fV21ZGFbZ9n3d95Vj1nVhWW1b+V1bZ7y+bgy7bvzKrQvLstq2scy6rSyvrxvDLux8W/iVmqratum6um7Ksq/Lui60dd1XRtf1fdW2fV+VZd+3hV9pG8OwjK6r+6os68Jry8ov67qw7MIvLKttK7+r68ow27qw3L6wLL/uC8uq277v6rrStXVluX2fsSu38QsAABhwAAAIMKEMFBqyIgCIEwBAEHIOKQahYgpCCKGkEEIqFWNSMuakZM5JKaWUFEpJrWJMSuaclMwxKaGUlkopqYRSWiqlxBRKaS2l1mJKqcVQSmulpNZKSa2llGJMrcUYMSYlc05K5pyUklJrJZXWMucoZQ5K6iCklEoqraTUYuacpA46Kx2E1EoqMZWUYgupxFZKaq2kFGMrMdXUWo4hpRhLSrGVlFptMdXWWqs1YkxK5pyUzDkqJaXWSiqtZc5J6iC01DkoqaTUYiopxco5SR2ElDLIqJSUWiupxBJSia20FGMpqcXUYq4pxRZDSS2WlFosqcTWYoy1tVRTJ6XFklKMJZUYW6y5ttZqDKXEVkqLsaSUW2sx1xZjjqGkFksrsZWUWmy15dhayzW1VGNKrdYWY40x5ZRrrT2n1mJNMdXaWqy51ZZbzLXnTkprpZQWS0oxttZijTHmHEppraQUWykpxtZara3FXEMpsZXSWiypxNhirLXFVmNqrcYWW62ltVprrb3GVlsurdXcYqw9tZRrrLXmWFNtBQAADDgAAASYUAYKDVkJAEQBAADGMMYYhEYpx5yT0ijlnHNSKucghJBS5hyEEFLKnINQSkuZcxBKSSmUklJqrYVSUmqttQIAAAocAAACbNCUWByg0JCVAEAqAIDBcTRNFFXVdX1fsSxRVFXXlW3jVyxNFFVVdm1b+DVRVFXXtW3bFn5NFFVVdmXZtoWiqrqybduybgvDqKqua9uybeuorqvbuq3bui9UXVmWbVu3dR3XtnXd9nVd+Bmzbeu2buu+8CMMR9/4IeTj+3RCCAAAT3AAACqwYXWEk6KxwEJDVgIAGQAAgDFKGYUYM0gxphhjTDHGmAAAgAEHAIAAE8pAoSErAoAoAADAOeecc84555xzzjnnnHPOOeecc44xxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY0wAwE6EA8BOhIVQaMhKACAcAABACCEpKaWUUkoRU85BSSmllFKqFIOMSkoppZRSpBR1lFJKKaWUIqWgpJJSSimllElJKaWUUkoppYw6SimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaVUSimllFJKKaWUUkoppRQAYPLgAACVYOMMK0lnhaPBhYasBAByAwAAhRiDEEJpraRUUkolVc5BKCWUlEpKKZWUUqqYgxBKKqmlklJKKbXSQSihlFBKKSWUUkooJYQQSgmhlFRCK6mEUkoHoYQSQimhhFRKKSWUzkEoIYUOQkmllNRCSB10VFIpIZVSSiklpZQ6CKGUklJLLZVSWkqpdBJSKamV1FJqqbWSUgmhpFZKSSWl0lpJJbUSSkklpZRSSymFVFJJJYSSUioltZZaSqm11lJIqZWUUkqppdRSSiWlkEpKqZSSUmollZRSaiGVlEpJKaTUSimlpFRCSamlUlpKLbWUSkmptFRSSaWUlEpJKaVSSksppRJKSqmllFpJKYWSUkoplZJSSyW1VEoKJaWUUkmptJRSSymVklIBAEAHDgAAAUZUWoidZlx5BI4oZJiAAgAAQABAgAkgMEBQMApBgDACAQAAAADAAAAfAABHARAR0ZzBAUKCwgJDg8MDAAAAAAAAAAAAAACAT2dnUwAEAAAAAAAAAADqnjMlAgAAADzQPmcBAQA=');
|
||||
if (mpeg) canPlayThrough('audio/mpeg;base64,/+MYxAAAAANIAUAAAASEEB/jwOFM/0MM/90b/+RhST//w4NFwOjf///PZu////9lns5GFDv//l9GlUIEEIAAAgIg8Ir/JGq3/+MYxDsLIj5QMYcoAP0dv9HIjUcH//yYSg+CIbkGP//8w0bLVjUP///3Z0x5QCAv/yLjwtGKTEFNRTMuOTeqqqqqqqqqqqqq/+MYxEkNmdJkUYc4AKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq');
|
||||
|
||||
/// lets find out!
|
||||
var time = (new Date()).getTime();
|
||||
var interval = window.setInterval(function() {
|
||||
var now = (new Date()).getTime();
|
||||
var maxExecution = now - time > 5000;
|
||||
if (!pending || maxExecution) {
|
||||
window.clearInterval(interval);
|
||||
onsuccess(supports);
|
||||
}
|
||||
}, 1);
|
||||
};
|
||||
|
||||
})(MIDI);
|
@ -1,161 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
GeneralMIDI
|
||||
----------------------------------------------------------
|
||||
*/
|
||||
|
||||
(function(root) { 'use strict';
|
||||
|
||||
root.GM = (function(arr) {
|
||||
var clean = function(name) {
|
||||
return name.replace(/[^a-z0-9 ]/gi, '').replace(/[ ]/g, '_').toLowerCase();
|
||||
};
|
||||
var res = {
|
||||
byName: { },
|
||||
byId: { },
|
||||
byCategory: { }
|
||||
};
|
||||
for (var key in arr) {
|
||||
var list = arr[key];
|
||||
for (var n = 0, length = list.length; n < length; n++) {
|
||||
var instrument = list[n];
|
||||
if (!instrument) continue;
|
||||
var num = parseInt(instrument.substr(0, instrument.indexOf(' ')), 10);
|
||||
instrument = instrument.replace(num + ' ', '');
|
||||
res.byId[--num] =
|
||||
res.byName[clean(instrument)] =
|
||||
res.byCategory[clean(key)] = {
|
||||
id: clean(instrument),
|
||||
instrument: instrument,
|
||||
number: num,
|
||||
category: key
|
||||
};
|
||||
}
|
||||
}
|
||||
return res;
|
||||
})({
|
||||
'Piano': ['1 Acoustic Grand Piano', '2 Bright Acoustic Piano', '3 Electric Grand Piano', '4 Honky-tonk Piano', '5 Electric Piano 1', '6 Electric Piano 2', '7 Harpsichord', '8 Clavinet'],
|
||||
'Chromatic Percussion': ['9 Celesta', '10 Glockenspiel', '11 Music Box', '12 Vibraphone', '13 Marimba', '14 Xylophone', '15 Tubular Bells', '16 Dulcimer'],
|
||||
'Organ': ['17 Drawbar Organ', '18 Percussive Organ', '19 Rock Organ', '20 Church Organ', '21 Reed Organ', '22 Accordion', '23 Harmonica', '24 Tango Accordion'],
|
||||
'Guitar': ['25 Acoustic Guitar (nylon)', '26 Acoustic Guitar (steel)', '27 Electric Guitar (jazz)', '28 Electric Guitar (clean)', '29 Electric Guitar (muted)', '30 Overdriven Guitar', '31 Distortion Guitar', '32 Guitar Harmonics'],
|
||||
'Bass': ['33 Acoustic Bass', '34 Electric Bass (finger)', '35 Electric Bass (pick)', '36 Fretless Bass', '37 Slap Bass 1', '38 Slap Bass 2', '39 Synth Bass 1', '40 Synth Bass 2'],
|
||||
'Strings': ['41 Violin', '42 Viola', '43 Cello', '44 Contrabass', '45 Tremolo Strings', '46 Pizzicato Strings', '47 Orchestral Harp', '48 Timpani'],
|
||||
'Ensemble': ['49 String Ensemble 1', '50 String Ensemble 2', '51 Synth Strings 1', '52 Synth Strings 2', '53 Choir Aahs', '54 Voice Oohs', '55 Synth Choir', '56 Orchestra Hit'],
|
||||
'Brass': ['57 Trumpet', '58 Trombone', '59 Tuba', '60 Muted Trumpet', '61 French Horn', '62 Brass Section', '63 Synth Brass 1', '64 Synth Brass 2'],
|
||||
'Reed': ['65 Soprano Sax', '66 Alto Sax', '67 Tenor Sax', '68 Baritone Sax', '69 Oboe', '70 English Horn', '71 Bassoon', '72 Clarinet'],
|
||||
'Pipe': ['73 Piccolo', '74 Flute', '75 Recorder', '76 Pan Flute', '77 Blown Bottle', '78 Shakuhachi', '79 Whistle', '80 Ocarina'],
|
||||
'Synth Lead': ['81 Lead 1 (square)', '82 Lead 2 (sawtooth)', '83 Lead 3 (calliope)', '84 Lead 4 (chiff)', '85 Lead 5 (charang)', '86 Lead 6 (voice)', '87 Lead 7 (fifths)', '88 Lead 8 (bass + lead)'],
|
||||
'Synth Pad': ['89 Pad 1 (new age)', '90 Pad 2 (warm)', '91 Pad 3 (polysynth)', '92 Pad 4 (choir)', '93 Pad 5 (bowed)', '94 Pad 6 (metallic)', '95 Pad 7 (halo)', '96 Pad 8 (sweep)'],
|
||||
'Synth Effects': ['97 FX 1 (rain)', '98 FX 2 (soundtrack)', '99 FX 3 (crystal)', '100 FX 4 (atmosphere)', '101 FX 5 (brightness)', '102 FX 6 (goblins)', '103 FX 7 (echoes)', '104 FX 8 (sci-fi)'],
|
||||
'Ethnic': ['105 Sitar', '106 Banjo', '107 Shamisen', '108 Koto', '109 Kalimba', '110 Bagpipe', '111 Fiddle', '112 Shanai'],
|
||||
'Percussive': ['113 Tinkle Bell', '114 Agogo', '115 Steel Drums', '116 Woodblock', '117 Taiko Drum', '118 Melodic Tom', '119 Synth Drum'],
|
||||
'Sound effects': ['120 Reverse Cymbal', '121 Guitar Fret Noise', '122 Breath Noise', '123 Seashore', '124 Bird Tweet', '125 Telephone Ring', '126 Helicopter', '127 Applause', '128 Gunshot']
|
||||
});
|
||||
|
||||
/* get/setInstrument
|
||||
--------------------------------------------------- */
|
||||
root.getInstrument = function(channelId) {
|
||||
var channel = root.channels[channelId];
|
||||
return channel && channel.instrument;
|
||||
};
|
||||
|
||||
root.setInstrument = function(channelId, program, delay) {
|
||||
var channel = root.channels[channelId];
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
channel.instrument = program;
|
||||
}, delay);
|
||||
} else {
|
||||
channel.instrument = program;
|
||||
}
|
||||
};
|
||||
|
||||
/* get/setMono
|
||||
--------------------------------------------------- */
|
||||
root.getMono = function(channelId) {
|
||||
var channel = root.channels[channelId];
|
||||
return channel && channel.mono;
|
||||
};
|
||||
|
||||
root.setMono = function(channelId, truthy, delay) {
|
||||
var channel = root.channels[channelId];
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
channel.mono = truthy;
|
||||
}, delay);
|
||||
} else {
|
||||
channel.mono = truthy;
|
||||
}
|
||||
};
|
||||
|
||||
/* get/setOmni
|
||||
--------------------------------------------------- */
|
||||
root.getOmni = function(channelId) {
|
||||
var channel = root.channels[channelId];
|
||||
return channel && channel.omni;
|
||||
};
|
||||
|
||||
root.setOmni = function(channelId, truthy) {
|
||||
var channel = root.channels[channelId];
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
channel.omni = truthy;
|
||||
}, delay);
|
||||
} else {
|
||||
channel.omni = truthy;
|
||||
}
|
||||
};
|
||||
|
||||
/* get/setSolo
|
||||
--------------------------------------------------- */
|
||||
root.getSolo = function(channelId) {
|
||||
var channel = root.channels[channelId];
|
||||
return channel && channel.solo;
|
||||
};
|
||||
|
||||
root.setSolo = function(channelId, truthy) {
|
||||
var channel = root.channels[channelId];
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
channel.solo = truthy;
|
||||
}, delay);
|
||||
} else {
|
||||
channel.solo = truthy;
|
||||
}
|
||||
};
|
||||
|
||||
/* channels
|
||||
--------------------------------------------------- */
|
||||
root.channels = (function() { // 0 - 15 channels
|
||||
var channels = {};
|
||||
for (var i = 0; i < 16; i++) {
|
||||
channels[i] = { // default values
|
||||
instrument: i,
|
||||
pitchBend: 0,
|
||||
mute: false,
|
||||
mono: false,
|
||||
omni: false,
|
||||
solo: false
|
||||
};
|
||||
}
|
||||
return channels;
|
||||
})();
|
||||
|
||||
/* note conversions
|
||||
--------------------------------------------------- */
|
||||
root.keyToNote = {}; // C8 == 108
|
||||
root.noteToKey = {}; // 108 == C8
|
||||
|
||||
(function() {
|
||||
var A0 = 0x15; // first note
|
||||
var C8 = 0x6C; // last note
|
||||
var number2key = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||
for (var n = A0; n <= C8; n++) {
|
||||
var octave = (n - 12) / 12 >> 0;
|
||||
var name = number2key[n % 12] + octave;
|
||||
root.keyToNote[name] = n;
|
||||
root.noteToKey[n] = name;
|
||||
}
|
||||
})();
|
||||
|
||||
})(MIDI);
|
@ -1,199 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
MIDI.Plugin : 0.3.4 : 2015-03-26
|
||||
----------------------------------------------------------
|
||||
https://github.com/mudcube/MIDI.js
|
||||
----------------------------------------------------------
|
||||
Inspired by javax.sound.midi (albeit a super simple version):
|
||||
http://docs.oracle.com/javase/6/docs/api/javax/sound/midi/package-summary.html
|
||||
----------------------------------------------------------
|
||||
Technologies
|
||||
----------------------------------------------------------
|
||||
Web MIDI API - no native support yet (jazzplugin)
|
||||
Web Audio API - firefox 25+, chrome 10+, safari 6+, opera 15+
|
||||
HTML5 Audio Tag - ie 9+, firefox 3.5+, chrome 4+, safari 4+, opera 9.5+, ios 4+, android 2.3+
|
||||
----------------------------------------------------------
|
||||
*/
|
||||
|
||||
if (typeof MIDI === 'undefined') MIDI = {};
|
||||
|
||||
MIDI.Soundfont = MIDI.Soundfont || {};
|
||||
MIDI.Player = MIDI.Player || {};
|
||||
|
||||
(function(root) { 'use strict';
|
||||
|
||||
root.DEBUG = true;
|
||||
root.USE_XHR = true;
|
||||
root.soundfontUrl = './soundfont/';
|
||||
|
||||
/*
|
||||
MIDI.loadPlugin({
|
||||
onsuccess: function() { },
|
||||
onprogress: function(state, percent) { },
|
||||
targetFormat: 'mp3', // optionally can force to use MP3 (for instance on mobile networks)
|
||||
instrument: 'acoustic_grand_piano', // or 1 (default)
|
||||
instruments: [ 'acoustic_grand_piano', 'acoustic_guitar_nylon' ] // or multiple instruments
|
||||
});
|
||||
*/
|
||||
|
||||
root.loadPlugin = function(opts) {
|
||||
if (typeof opts === 'function') {
|
||||
opts = {onsuccess: opts};
|
||||
}
|
||||
|
||||
root.soundfontUrl = opts.soundfontUrl || root.soundfontUrl;
|
||||
|
||||
/// Detect the best type of audio to use
|
||||
root.audioDetect(function(supports) {
|
||||
var hash = window.location.hash;
|
||||
var api = '';
|
||||
|
||||
/// use the most appropriate plugin if not specified
|
||||
if (supports[opts.api]) {
|
||||
api = opts.api;
|
||||
} else if (supports[hash.substr(1)]) {
|
||||
api = hash.substr(1);
|
||||
} else if (supports.webmidi) {
|
||||
api = 'webmidi';
|
||||
} else if (window.AudioContext) { // Chrome
|
||||
api = 'webaudio';
|
||||
} else if (window.Audio) { // Firefox
|
||||
api = 'audiotag';
|
||||
}
|
||||
|
||||
if (connect[api]) {
|
||||
/// use audio/ogg when supported
|
||||
if (opts.targetFormat) {
|
||||
var audioFormat = opts.targetFormat;
|
||||
} else { // use best quality
|
||||
var audioFormat = supports['audio/ogg'] ? 'ogg' : 'mp3';
|
||||
}
|
||||
|
||||
/// load the specified plugin
|
||||
root.__api = api;
|
||||
root.__audioFormat = audioFormat;
|
||||
root.supports = supports;
|
||||
root.loadResource(opts);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
root.loadResource({
|
||||
onsuccess: function() { },
|
||||
onprogress: function(state, percent) { },
|
||||
instrument: 'banjo'
|
||||
})
|
||||
*/
|
||||
|
||||
root.loadResource = function(opts) {
|
||||
var instruments = opts.instruments || opts.instrument || 'acoustic_grand_piano';
|
||||
///
|
||||
if (typeof instruments !== 'object') {
|
||||
if (instruments || instruments === 0) {
|
||||
instruments = [instruments];
|
||||
} else {
|
||||
instruments = [];
|
||||
}
|
||||
}
|
||||
/// convert numeric ids into strings
|
||||
for (var i = 0; i < instruments.length; i ++) {
|
||||
var instrument = instruments[i];
|
||||
if (instrument === +instrument) { // is numeric
|
||||
if (root.GM.byId[instrument]) {
|
||||
instruments[i] = root.GM.byId[instrument].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
///
|
||||
opts.format = root.__audioFormat;
|
||||
opts.instruments = instruments;
|
||||
///
|
||||
connect[root.__api](opts);
|
||||
};
|
||||
|
||||
var connect = {
|
||||
webmidi: function(opts) {
|
||||
// cant wait for this to be standardized!
|
||||
root.WebMIDI.connect(opts);
|
||||
},
|
||||
audiotag: function(opts) {
|
||||
// works ok, kinda like a drunken tuna fish, across the board
|
||||
// http://caniuse.com/audio
|
||||
requestQueue(opts, 'AudioTag');
|
||||
},
|
||||
webaudio: function(opts) {
|
||||
// works awesome! safari, chrome and firefox support
|
||||
// http://caniuse.com/web-audio
|
||||
requestQueue(opts, 'WebAudio');
|
||||
}
|
||||
};
|
||||
|
||||
var requestQueue = function(opts, context) {
|
||||
var audioFormat = opts.format;
|
||||
var instruments = opts.instruments;
|
||||
var onprogress = opts.onprogress;
|
||||
var onerror = opts.onerror;
|
||||
///
|
||||
var length = instruments.length;
|
||||
var pending = length;
|
||||
var waitForEnd = function() {
|
||||
if (!--pending) {
|
||||
onprogress && onprogress('load', 1.0);
|
||||
root[context].connect(opts);
|
||||
}
|
||||
};
|
||||
///
|
||||
for (var i = 0; i < length; i ++) {
|
||||
var instrumentId = instruments[i];
|
||||
if (MIDI.Soundfont[instrumentId]) { // already loaded
|
||||
waitForEnd();
|
||||
} else { // needs to be requested
|
||||
sendRequest(instruments[i], audioFormat, function(evt, progress) {
|
||||
var fileProgress = progress / length;
|
||||
var queueProgress = (length - pending) / length;
|
||||
onprogress && onprogress('load', fileProgress + queueProgress, instrumentId);
|
||||
}, function() {
|
||||
waitForEnd();
|
||||
}, onerror);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var sendRequest = function(instrumentId, audioFormat, onprogress, onsuccess, onerror) {
|
||||
var soundfontPath = root.soundfontUrl + instrumentId + '-' + audioFormat + '.js';
|
||||
if (root.USE_XHR) {
|
||||
root.util.request({
|
||||
url: soundfontPath,
|
||||
format: 'text',
|
||||
onerror: onerror,
|
||||
onprogress: onprogress,
|
||||
onsuccess: function(event, responseText) {
|
||||
var script = document.createElement('script');
|
||||
script.language = 'javascript';
|
||||
script.type = 'text/javascript';
|
||||
script.text = responseText;
|
||||
document.body.appendChild(script);
|
||||
///
|
||||
onsuccess();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dom.loadScript.add({
|
||||
url: soundfontPath,
|
||||
verify: 'MIDI.Soundfont["' + instrumentId + '"]',
|
||||
onerror: onerror,
|
||||
onsuccess: function() {
|
||||
onsuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
root.setDefaultPlugin = function(midi) {
|
||||
for (var key in midi) {
|
||||
root[key] = midi[key];
|
||||
}
|
||||
};
|
||||
|
||||
})(MIDI);
|
@ -1,380 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
MIDI.Player : 0.3.1 : 2015-03-26
|
||||
----------------------------------------------------------
|
||||
https://github.com/mudcube/MIDI.js
|
||||
----------------------------------------------------------
|
||||
*/
|
||||
|
||||
if (typeof MIDI === 'undefined') MIDI = {};
|
||||
if (typeof MIDI.Player === 'undefined') MIDI.Player = {};
|
||||
|
||||
(function() { 'use strict';
|
||||
|
||||
var midi = MIDI.Player;
|
||||
midi.currentTime = 0;
|
||||
midi.endTime = 0;
|
||||
midi.restart = 0;
|
||||
midi.playing = false;
|
||||
midi.timeWarp = 1;
|
||||
midi.startDelay = 0;
|
||||
midi.BPM = 120;
|
||||
|
||||
midi.start =
|
||||
midi.resume = function(onsuccess) {
|
||||
if (midi.currentTime < -1) {
|
||||
midi.currentTime = -1;
|
||||
}
|
||||
startAudio(midi.currentTime, null, onsuccess);
|
||||
};
|
||||
|
||||
midi.pause = function() {
|
||||
var tmp = midi.restart;
|
||||
stopAudio();
|
||||
midi.restart = tmp;
|
||||
};
|
||||
|
||||
midi.stop = function() {
|
||||
stopAudio();
|
||||
midi.restart = 0;
|
||||
midi.currentTime = 0;
|
||||
};
|
||||
|
||||
midi.addListener = function(onsuccess) {
|
||||
onMidiEvent = onsuccess;
|
||||
};
|
||||
|
||||
midi.removeListener = function() {
|
||||
onMidiEvent = undefined;
|
||||
};
|
||||
|
||||
midi.clearAnimation = function() {
|
||||
if (midi.animationFrameId) {
|
||||
cancelAnimationFrame(midi.animationFrameId);
|
||||
}
|
||||
};
|
||||
|
||||
midi.setAnimation = function(callback) {
|
||||
var currentTime = 0;
|
||||
var tOurTime = 0;
|
||||
var tTheirTime = 0;
|
||||
//
|
||||
midi.clearAnimation();
|
||||
///
|
||||
var frame = function() {
|
||||
midi.animationFrameId = requestAnimationFrame(frame);
|
||||
///
|
||||
if (midi.endTime === 0) {
|
||||
return;
|
||||
}
|
||||
if (midi.playing) {
|
||||
currentTime = (tTheirTime === midi.currentTime) ? tOurTime - Date.now() : 0;
|
||||
if (midi.currentTime === 0) {
|
||||
currentTime = 0;
|
||||
} else {
|
||||
currentTime = midi.currentTime - currentTime;
|
||||
}
|
||||
if (tTheirTime !== midi.currentTime) {
|
||||
tOurTime = Date.now();
|
||||
tTheirTime = midi.currentTime;
|
||||
}
|
||||
} else { // paused
|
||||
currentTime = midi.currentTime;
|
||||
}
|
||||
///
|
||||
var endTime = midi.endTime;
|
||||
var percent = currentTime / endTime;
|
||||
var total = currentTime / 1000;
|
||||
var minutes = total / 60;
|
||||
var seconds = total - (minutes * 60);
|
||||
var t1 = minutes * 60 + seconds;
|
||||
var t2 = (endTime / 1000);
|
||||
///
|
||||
if (t2 - t1 < -1.0) {
|
||||
return;
|
||||
} else {
|
||||
callback({
|
||||
now: t1,
|
||||
end: t2,
|
||||
events: noteRegistrar
|
||||
});
|
||||
}
|
||||
};
|
||||
///
|
||||
requestAnimationFrame(frame);
|
||||
};
|
||||
|
||||
// helpers
|
||||
|
||||
midi.loadMidiFile = function(onsuccess, onprogress, onerror) {
|
||||
try {
|
||||
midi.replayer = new Replayer(MidiFile(midi.currentData), midi.timeWarp, null, midi.BPM);
|
||||
midi.data = midi.replayer.getData();
|
||||
midi.endTime = getLength();
|
||||
///
|
||||
MIDI.loadPlugin({
|
||||
// instruments: midi.getFileInstruments(),
|
||||
onsuccess: onsuccess,
|
||||
onprogress: onprogress,
|
||||
onerror: onerror
|
||||
});
|
||||
} catch(event) {
|
||||
onerror && onerror(event);
|
||||
}
|
||||
};
|
||||
|
||||
midi.loadFile = function(file, onsuccess, onprogress, onerror) {
|
||||
midi.stop();
|
||||
if (file.indexOf('base64,') !== -1) {
|
||||
var data = window.atob(file.split(',')[1]);
|
||||
midi.currentData = data;
|
||||
midi.loadMidiFile(onsuccess, onprogress, onerror);
|
||||
} else {
|
||||
var fetch = new XMLHttpRequest();
|
||||
fetch.open('GET', file);
|
||||
fetch.overrideMimeType('text/plain; charset=x-user-defined');
|
||||
fetch.onreadystatechange = function() {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
var t = this.responseText || '';
|
||||
var ff = [];
|
||||
var mx = t.length;
|
||||
var scc = String.fromCharCode;
|
||||
for (var z = 0; z < mx; z++) {
|
||||
ff[z] = scc(t.charCodeAt(z) & 255);
|
||||
}
|
||||
///
|
||||
var data = ff.join('');
|
||||
midi.currentData = data;
|
||||
midi.loadMidiFile(onsuccess, onprogress, onerror);
|
||||
} else {
|
||||
onerror && onerror('Unable to load MIDI file');
|
||||
}
|
||||
}
|
||||
};
|
||||
fetch.send();
|
||||
}
|
||||
};
|
||||
|
||||
midi.getFileInstruments = function() {
|
||||
var instruments = {};
|
||||
var programs = {};
|
||||
for (var n = 0; n < midi.data.length; n ++) {
|
||||
var event = midi.data[n][0].event;
|
||||
if (event.type !== 'channel') {
|
||||
continue;
|
||||
}
|
||||
var channel = event.channel;
|
||||
switch(event.subtype) {
|
||||
case 'controller':
|
||||
// console.log(event.channel, MIDI.defineControl[event.controllerType], event.value);
|
||||
break;
|
||||
case 'programChange':
|
||||
programs[channel] = event.programNumber;
|
||||
break;
|
||||
case 'noteOn':
|
||||
var program = programs[channel];
|
||||
var gm = MIDI.GM.byId[isFinite(program) ? program : channel];
|
||||
instruments[gm.id] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var ret = [];
|
||||
for (var key in instruments) {
|
||||
ret.push(key);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Playing the audio
|
||||
|
||||
var eventQueue = []; // hold events to be triggered
|
||||
var queuedTime; //
|
||||
var startTime = 0; // to measure time elapse
|
||||
var noteRegistrar = {}; // get event for requested note
|
||||
var onMidiEvent = undefined; // listener
|
||||
var scheduleTracking = function(channel, note, currentTime, offset, message, velocity, time) {
|
||||
return setTimeout(function() {
|
||||
var data = {
|
||||
channel: channel,
|
||||
note: note,
|
||||
now: currentTime,
|
||||
end: midi.endTime,
|
||||
message: message,
|
||||
velocity: velocity
|
||||
};
|
||||
//
|
||||
if (message === 128) {
|
||||
delete noteRegistrar[note];
|
||||
} else {
|
||||
noteRegistrar[note] = data;
|
||||
}
|
||||
if (onMidiEvent) {
|
||||
onMidiEvent(data);
|
||||
}
|
||||
midi.currentTime = currentTime;
|
||||
///
|
||||
eventQueue.shift();
|
||||
///
|
||||
if (eventQueue.length < 1000) {
|
||||
startAudio(queuedTime, true);
|
||||
} else if (midi.currentTime === queuedTime && queuedTime < midi.endTime) { // grab next sequence
|
||||
startAudio(queuedTime, true);
|
||||
}
|
||||
}, currentTime - offset);
|
||||
};
|
||||
|
||||
var getContext = function() {
|
||||
if (MIDI.api === 'webaudio') {
|
||||
return MIDI.WebAudio.getContext();
|
||||
} else {
|
||||
midi.ctx = {currentTime: 0};
|
||||
}
|
||||
return midi.ctx;
|
||||
};
|
||||
|
||||
var getLength = function() {
|
||||
var data = midi.data;
|
||||
var length = data.length;
|
||||
var totalTime = 0.5;
|
||||
for (var n = 0; n < length; n++) {
|
||||
totalTime += data[n][1];
|
||||
}
|
||||
return totalTime;
|
||||
};
|
||||
|
||||
var __now;
|
||||
var getNow = function() {
|
||||
if (window.performance && window.performance.now) {
|
||||
return window.performance.now();
|
||||
} else {
|
||||
return Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
var startAudio = function(currentTime, fromCache, onsuccess) {
|
||||
if (!midi.replayer) {
|
||||
return;
|
||||
}
|
||||
if (!fromCache) {
|
||||
if (typeof currentTime === 'undefined') {
|
||||
currentTime = midi.restart;
|
||||
}
|
||||
///
|
||||
midi.playing && stopAudio();
|
||||
midi.playing = true;
|
||||
midi.data = midi.replayer.getData();
|
||||
midi.endTime = getLength();
|
||||
}
|
||||
///
|
||||
var note;
|
||||
var offset = 0;
|
||||
var messages = 0;
|
||||
var data = midi.data;
|
||||
var ctx = getContext();
|
||||
var length = data.length;
|
||||
//
|
||||
queuedTime = 0.5;
|
||||
///
|
||||
var interval = eventQueue[0] && eventQueue[0].interval || 0;
|
||||
var foffset = currentTime - midi.currentTime;
|
||||
///
|
||||
if (MIDI.api !== 'webaudio') { // set currentTime on ctx
|
||||
var now = getNow();
|
||||
__now = __now || now;
|
||||
ctx.currentTime = (now - __now) / 1000;
|
||||
}
|
||||
///
|
||||
startTime = ctx.currentTime;
|
||||
///
|
||||
for (var n = 0; n < length && messages < 100; n++) {
|
||||
var obj = data[n];
|
||||
if ((queuedTime += obj[1]) <= currentTime) {
|
||||
offset = queuedTime;
|
||||
continue;
|
||||
}
|
||||
///
|
||||
currentTime = queuedTime - offset;
|
||||
///
|
||||
var event = obj[0].event;
|
||||
if (event.type !== 'channel') {
|
||||
continue;
|
||||
}
|
||||
///
|
||||
var channelId = event.channel;
|
||||
var channel = MIDI.channels[channelId];
|
||||
var delay = ctx.currentTime + ((currentTime + foffset + midi.startDelay) / 1000);
|
||||
var queueTime = queuedTime - offset + midi.startDelay;
|
||||
switch (event.subtype) {
|
||||
case 'controller':
|
||||
MIDI.setController(channelId, event.controllerType, event.value, delay);
|
||||
break;
|
||||
case 'programChange':
|
||||
MIDI.programChange(channelId, event.programNumber, delay);
|
||||
break;
|
||||
case 'pitchBend':
|
||||
MIDI.pitchBend(channelId, event.value, delay);
|
||||
break;
|
||||
case 'noteOn':
|
||||
if (channel.mute) break;
|
||||
note = event.noteNumber - (midi.MIDIOffset || 0);
|
||||
eventQueue.push({
|
||||
event: event,
|
||||
time: queueTime,
|
||||
source: MIDI.noteOn(channelId, event.noteNumber, event.velocity, delay),
|
||||
interval: scheduleTracking(channelId, note, queuedTime + midi.startDelay, offset - foffset, 144, event.velocity)
|
||||
});
|
||||
messages++;
|
||||
break;
|
||||
case 'noteOff':
|
||||
if (channel.mute) break;
|
||||
note = event.noteNumber - (midi.MIDIOffset || 0);
|
||||
eventQueue.push({
|
||||
event: event,
|
||||
time: queueTime,
|
||||
source: MIDI.noteOff(channelId, event.noteNumber, delay),
|
||||
interval: scheduleTracking(channelId, note, queuedTime, offset - foffset, 128, 0)
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
///
|
||||
onsuccess && onsuccess(eventQueue);
|
||||
};
|
||||
|
||||
var stopAudio = function() {
|
||||
var ctx = getContext();
|
||||
midi.playing = false;
|
||||
midi.restart += (ctx.currentTime - startTime) * 1000;
|
||||
// stop the audio, and intervals
|
||||
while (eventQueue.length) {
|
||||
var o = eventQueue.pop();
|
||||
window.clearInterval(o.interval);
|
||||
if (!o.source) continue; // is not webaudio
|
||||
if (typeof(o.source) === 'number') {
|
||||
window.clearTimeout(o.source);
|
||||
} else { // webaudio
|
||||
o.source.disconnect(0);
|
||||
}
|
||||
}
|
||||
// run callback to cancel any notes still playing
|
||||
for (var key in noteRegistrar) {
|
||||
var o = noteRegistrar[key]
|
||||
if (noteRegistrar[key].message === 144 && onMidiEvent) {
|
||||
onMidiEvent({
|
||||
channel: o.channel,
|
||||
note: o.note,
|
||||
now: o.now,
|
||||
end: o.end,
|
||||
message: 128,
|
||||
velocity: o.velocity
|
||||
});
|
||||
}
|
||||
}
|
||||
// reset noteRegistrar
|
||||
noteRegistrar = {};
|
||||
};
|
||||
|
||||
})();
|
@ -1,150 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------------------
|
||||
AudioTag <audio> - OGG or MPEG Soundbank
|
||||
----------------------------------------------------------------------
|
||||
http://dev.w3.org/html5/spec/Overview.html#the-audio-element
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
(function(root) { 'use strict';
|
||||
|
||||
window.Audio && (function() {
|
||||
var midi = root.AudioTag = { api: 'audiotag' };
|
||||
var noteToKey = {};
|
||||
var volume = 127; // floating point
|
||||
var buffer_nid = -1; // current channel
|
||||
var audioBuffers = []; // the audio channels
|
||||
var notesOn = []; // instrumentId + noteId that is currently playing in each 'channel', for routing noteOff/chordOff calls
|
||||
var notes = {}; // the piano keys
|
||||
for (var nid = 0; nid < 12; nid ++) {
|
||||
audioBuffers[nid] = new Audio();
|
||||
}
|
||||
|
||||
var playChannel = function(channel, note) {
|
||||
if (!root.channels[channel]) return;
|
||||
var instrument = root.channels[channel].instrument;
|
||||
var instrumentId = root.GM.byId[instrument].id;
|
||||
var note = notes[note];
|
||||
if (note) {
|
||||
var instrumentNoteId = instrumentId + '' + note.id;
|
||||
var nid = (buffer_nid + 1) % audioBuffers.length;
|
||||
var audio = audioBuffers[nid];
|
||||
notesOn[ nid ] = instrumentNoteId;
|
||||
if (!root.Soundfont[instrumentId]) {
|
||||
if (root.DEBUG) {
|
||||
console.log('404', instrumentId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
audio.src = root.Soundfont[instrumentId][note.id];
|
||||
audio.volume = volume / 127;
|
||||
audio.play();
|
||||
buffer_nid = nid;
|
||||
}
|
||||
};
|
||||
|
||||
var stopChannel = function(channel, note) {
|
||||
if (!root.channels[channel]) return;
|
||||
var instrument = root.channels[channel].instrument;
|
||||
var instrumentId = root.GM.byId[instrument].id;
|
||||
var note = notes[note];
|
||||
if (note) {
|
||||
var instrumentNoteId = instrumentId + '' + note.id;
|
||||
for (var i = 0, len = audioBuffers.length; i < len; i++) {
|
||||
var nid = (i + buffer_nid + 1) % len;
|
||||
var cId = notesOn[nid];
|
||||
if (cId && cId == instrumentNoteId) {
|
||||
audioBuffers[nid].pause();
|
||||
notesOn[nid] = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
midi.audioBuffers = audioBuffers;
|
||||
midi.send = function(data, delay) { };
|
||||
midi.setController = function(channel, type, value, delay) { };
|
||||
midi.setVolume = function(channel, n) {
|
||||
volume = n; //- should be channel specific volume
|
||||
};
|
||||
|
||||
midi.programChange = function(channel, program) {
|
||||
root.channels[channel].instrument = program;
|
||||
};
|
||||
|
||||
midi.pitchBend = function(channel, program, delay) { };
|
||||
|
||||
midi.noteOn = function(channel, note, velocity, delay) {
|
||||
var id = noteToKey[note];
|
||||
if (!notes[id]) return;
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
playChannel(channel, id);
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
playChannel(channel, id);
|
||||
}
|
||||
};
|
||||
|
||||
midi.noteOff = function(channel, note, delay) {
|
||||
// var id = noteToKey[note];
|
||||
// if (!notes[id]) return;
|
||||
// if (delay) {
|
||||
// return setTimeout(function() {
|
||||
// stopChannel(channel, id);
|
||||
// }, delay * 1000)
|
||||
// } else {
|
||||
// stopChannel(channel, id);
|
||||
// }
|
||||
};
|
||||
|
||||
midi.chordOn = function(channel, chord, velocity, delay) {
|
||||
for (var idx = 0; idx < chord.length; idx ++) {
|
||||
var n = chord[idx];
|
||||
var id = noteToKey[n];
|
||||
if (!notes[id]) continue;
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
playChannel(channel, id);
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
playChannel(channel, id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
midi.chordOff = function(channel, chord, delay) {
|
||||
for (var idx = 0; idx < chord.length; idx ++) {
|
||||
var n = chord[idx];
|
||||
var id = noteToKey[n];
|
||||
if (!notes[id]) continue;
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
stopChannel(channel, id);
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
stopChannel(channel, id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
midi.stopAllNotes = function() {
|
||||
for (var nid = 0, length = audioBuffers.length; nid < length; nid++) {
|
||||
audioBuffers[nid].pause();
|
||||
}
|
||||
};
|
||||
|
||||
midi.connect = function(opts) {
|
||||
root.setDefaultPlugin(midi);
|
||||
///
|
||||
for (var key in root.keyToNote) {
|
||||
noteToKey[root.keyToNote[key]] = key;
|
||||
notes[key] = {id: key};
|
||||
}
|
||||
///
|
||||
opts.onsuccess && opts.onsuccess();
|
||||
};
|
||||
})();
|
||||
|
||||
})(MIDI);
|
@ -1,326 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
Web Audio API - OGG or MPEG Soundbank
|
||||
----------------------------------------------------------
|
||||
http://webaudio.github.io/web-audio-api/
|
||||
----------------------------------------------------------
|
||||
*/
|
||||
|
||||
(function(root) { 'use strict';
|
||||
|
||||
window.AudioContext && (function() {
|
||||
var audioContext = null; // new AudioContext();
|
||||
var useStreamingBuffer = false; // !!audioContext.createMediaElementSource;
|
||||
var midi = root.WebAudio = {api: 'webaudio'};
|
||||
var ctx; // audio context
|
||||
var sources = {};
|
||||
var effects = {};
|
||||
var masterVolume = 127;
|
||||
var audioBuffers = {};
|
||||
///
|
||||
midi.audioBuffers = audioBuffers;
|
||||
midi.send = function(data, delay) { };
|
||||
midi.setController = function(channelId, type, value, delay) { };
|
||||
|
||||
midi.setVolume = function(channelId, volume, delay) {
|
||||
if (delay) {
|
||||
setTimeout(function() {
|
||||
masterVolume = volume;
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
masterVolume = volume;
|
||||
}
|
||||
};
|
||||
|
||||
midi.programChange = function(channelId, program, delay) {
|
||||
// if (delay) {
|
||||
// return setTimeout(function() {
|
||||
// var channel = root.channels[channelId];
|
||||
// channel.instrument = program;
|
||||
// }, delay);
|
||||
// } else {
|
||||
var channel = root.channels[channelId];
|
||||
channel.instrument = program;
|
||||
// }
|
||||
};
|
||||
|
||||
midi.pitchBend = function(channelId, program, delay) {
|
||||
// if (delay) {
|
||||
// setTimeout(function() {
|
||||
// var channel = root.channels[channelId];
|
||||
// channel.pitchBend = program;
|
||||
// }, delay);
|
||||
// } else {
|
||||
var channel = root.channels[channelId];
|
||||
channel.pitchBend = program;
|
||||
// }
|
||||
};
|
||||
|
||||
midi.noteOn = function(channelId, noteId, velocity, delay) {
|
||||
delay = delay || 0;
|
||||
|
||||
/// check whether the note exists
|
||||
var channel = root.channels[channelId];
|
||||
var instrument = channel.instrument;
|
||||
var bufferId = instrument + '' + noteId;
|
||||
var buffer = audioBuffers[bufferId];
|
||||
if (!buffer) {
|
||||
// console.log(MIDI.GM.byId[instrument].id, instrument, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
/// convert relative delay to absolute delay
|
||||
if (delay < ctx.currentTime) {
|
||||
delay += ctx.currentTime;
|
||||
}
|
||||
|
||||
/// create audio buffer
|
||||
if (useStreamingBuffer) {
|
||||
var source = ctx.createMediaElementSource(buffer);
|
||||
} else { // XMLHTTP buffer
|
||||
var source = ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
}
|
||||
|
||||
/// add effects to buffer
|
||||
if (effects) {
|
||||
var chain = source;
|
||||
for (var key in effects) {
|
||||
chain.connect(effects[key].input);
|
||||
chain = effects[key];
|
||||
}
|
||||
}
|
||||
|
||||
/// add gain + pitchShift
|
||||
var gain = (velocity / 127) * (masterVolume / 127) * 2 - 1;
|
||||
source.connect(ctx.destination);
|
||||
source.playbackRate.value = 1; // pitch shift
|
||||
source.gainNode = ctx.createGain(); // gain
|
||||
source.gainNode.connect(ctx.destination);
|
||||
source.gainNode.gain.value = Math.min(1.0, Math.max(-1.0, gain));
|
||||
source.connect(source.gainNode);
|
||||
///
|
||||
if (useStreamingBuffer) {
|
||||
if (delay) {
|
||||
return setTimeout(function() {
|
||||
buffer.currentTime = 0;
|
||||
buffer.play()
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
buffer.currentTime = 0;
|
||||
buffer.play()
|
||||
}
|
||||
} else {
|
||||
source.start(delay || 0);
|
||||
}
|
||||
///
|
||||
sources[channelId + '' + noteId] = source;
|
||||
///
|
||||
return source;
|
||||
};
|
||||
|
||||
midi.noteOff = function(channelId, noteId, delay) {
|
||||
delay = delay || 0;
|
||||
|
||||
/// check whether the note exists
|
||||
var channel = root.channels[channelId];
|
||||
var instrument = channel.instrument;
|
||||
var bufferId = instrument + '' + noteId;
|
||||
var buffer = audioBuffers[bufferId];
|
||||
if (buffer) {
|
||||
if (delay < ctx.currentTime) {
|
||||
delay += ctx.currentTime;
|
||||
}
|
||||
///
|
||||
var source = sources[channelId + '' + noteId];
|
||||
if (source) {
|
||||
if (source.gainNode) {
|
||||
// @Miranet: 'the values of 0.2 and 0.3 could of course be used as
|
||||
// a 'release' parameter for ADSR like time settings.'
|
||||
// add { 'metadata': { release: 0.3 } } to soundfont files
|
||||
var gain = source.gainNode.gain;
|
||||
gain.linearRampToValueAtTime(gain.value, delay);
|
||||
gain.linearRampToValueAtTime(-1.0, delay + 0.3);
|
||||
}
|
||||
///
|
||||
if (useStreamingBuffer) {
|
||||
if (delay) {
|
||||
setTimeout(function() {
|
||||
buffer.pause();
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
buffer.pause();
|
||||
}
|
||||
} else {
|
||||
if (source.noteOff) {
|
||||
source.noteOff(delay + 0.5);
|
||||
} else {
|
||||
source.stop(delay + 0.5);
|
||||
}
|
||||
}
|
||||
///
|
||||
delete sources[channelId + '' + noteId];
|
||||
///
|
||||
return source;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
midi.chordOn = function(channel, chord, velocity, delay) {
|
||||
var res = {};
|
||||
for (var n = 0, note, len = chord.length; n < len; n++) {
|
||||
res[note = chord[n]] = midi.noteOn(channel, note, velocity, delay);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
midi.chordOff = function(channel, chord, delay) {
|
||||
var res = {};
|
||||
for (var n = 0, note, len = chord.length; n < len; n++) {
|
||||
res[note = chord[n]] = midi.noteOff(channel, note, delay);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
midi.stopAllNotes = function() {
|
||||
for (var sid in sources) {
|
||||
var delay = 0;
|
||||
if (delay < ctx.currentTime) {
|
||||
delay += ctx.currentTime;
|
||||
}
|
||||
var source = sources[sid];
|
||||
source.gain.linearRampToValueAtTime(1, delay);
|
||||
source.gain.linearRampToValueAtTime(0, delay + 0.3);
|
||||
if (source.noteOff) { // old api
|
||||
source.noteOff(delay + 0.3);
|
||||
} else { // new api
|
||||
source.stop(delay + 0.3);
|
||||
}
|
||||
delete sources[sid];
|
||||
}
|
||||
};
|
||||
|
||||
midi.setEffects = function(list) {
|
||||
if (ctx.tunajs) {
|
||||
for (var n = 0; n < list.length; n ++) {
|
||||
var data = list[n];
|
||||
var effect = new ctx.tunajs[data.type](data);
|
||||
effect.connect(ctx.destination);
|
||||
effects[data.type] = effect;
|
||||
}
|
||||
} else {
|
||||
return console.log('Effects module not installed.');
|
||||
}
|
||||
};
|
||||
|
||||
midi.connect = function(opts) {
|
||||
root.setDefaultPlugin(midi);
|
||||
midi.setContext(ctx || createAudioContext(), opts.onsuccess);
|
||||
};
|
||||
|
||||
midi.getContext = function() {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
midi.setContext = function(newCtx, onload, onprogress, onerror) {
|
||||
ctx = newCtx;
|
||||
|
||||
/// tuna.js effects module - https://github.com/Dinahmoe/tuna
|
||||
if (typeof Tuna !== 'undefined' && !ctx.tunajs) {
|
||||
ctx.tunajs = new Tuna(ctx);
|
||||
}
|
||||
|
||||
/// loading audio files
|
||||
var urls = [];
|
||||
var notes = root.keyToNote;
|
||||
for (var key in notes) urls.push(key);
|
||||
///
|
||||
var waitForEnd = function(instrument) {
|
||||
for (var key in bufferPending) { // has pending items
|
||||
if (bufferPending[key]) return;
|
||||
}
|
||||
///
|
||||
if (onload) { // run onload once
|
||||
onload();
|
||||
onload = null;
|
||||
}
|
||||
};
|
||||
///
|
||||
var requestAudio = function(soundfont, instrumentId, index, key) {
|
||||
var url = soundfont[key];
|
||||
if (url) {
|
||||
bufferPending[instrumentId] ++;
|
||||
loadAudio(url, function(buffer) {
|
||||
buffer.id = key;
|
||||
var noteId = root.keyToNote[key];
|
||||
audioBuffers[instrumentId + '' + noteId] = buffer;
|
||||
///
|
||||
if (-- bufferPending[instrumentId] === 0) {
|
||||
var percent = index / 87;
|
||||
// console.log(MIDI.GM.byId[instrumentId], 'processing: ', percent);
|
||||
soundfont.isLoaded = true;
|
||||
waitForEnd(instrument);
|
||||
}
|
||||
}, function(err) {
|
||||
// console.log(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
///
|
||||
var bufferPending = {};
|
||||
for (var instrument in root.Soundfont) {
|
||||
var soundfont = root.Soundfont[instrument];
|
||||
if (soundfont.isLoaded) {
|
||||
continue;
|
||||
}
|
||||
///
|
||||
var synth = root.GM.byName[instrument];
|
||||
var instrumentId = synth.number;
|
||||
///
|
||||
bufferPending[instrumentId] = 0;
|
||||
///
|
||||
for (var index = 0; index < urls.length; index++) {
|
||||
var key = urls[index];
|
||||
requestAudio(soundfont, instrumentId, index, key);
|
||||
}
|
||||
}
|
||||
///
|
||||
setTimeout(waitForEnd, 1);
|
||||
};
|
||||
|
||||
/* Load audio file: streaming | base64 | arraybuffer
|
||||
---------------------------------------------------------------------- */
|
||||
function loadAudio(url, onload, onerror) {
|
||||
if (useStreamingBuffer) {
|
||||
var audio = new Audio();
|
||||
audio.src = url;
|
||||
audio.controls = false;
|
||||
audio.autoplay = false;
|
||||
audio.preload = false;
|
||||
audio.addEventListener('canplay', function() {
|
||||
onload && onload(audio);
|
||||
});
|
||||
audio.addEventListener('error', function(err) {
|
||||
onerror && onerror(err);
|
||||
});
|
||||
document.body.appendChild(audio);
|
||||
} else if (url.indexOf('data:audio') === 0) { // Base64 string
|
||||
var base64 = url.split(',')[1];
|
||||
var buffer = Base64Binary.decodeArrayBuffer(base64);
|
||||
ctx.decodeAudioData(buffer, onload, onerror);
|
||||
} else { // XMLHTTP buffer
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('GET', url, true);
|
||||
request.responseType = 'arraybuffer';
|
||||
request.onload = function() {
|
||||
ctx.decodeAudioData(request.response, onload, onerror);
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
};
|
||||
|
||||
function createAudioContext() {
|
||||
return new (window.AudioContext || window.webkitAudioContext)();
|
||||
};
|
||||
})();
|
||||
})(MIDI);
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------------------
|
||||
Web MIDI API - Native Soundbanks
|
||||
----------------------------------------------------------------------
|
||||
http://webaudio.github.io/web-midi-api/
|
||||
----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
(function(root) { 'use strict';
|
||||
|
||||
var plugin = null;
|
||||
var output = null;
|
||||
var channels = [];
|
||||
var midi = root.WebMIDI = {api: 'webmidi'};
|
||||
midi.send = function(data, delay) { // set channel volume
|
||||
output.send(data, delay * 1000);
|
||||
};
|
||||
|
||||
midi.setController = function(channel, type, value, delay) {
|
||||
output.send([channel, type, value], delay * 1000);
|
||||
};
|
||||
|
||||
midi.setVolume = function(channel, volume, delay) { // set channel volume
|
||||
output.send([0xB0 + channel, 0x07, volume], delay * 1000);
|
||||
};
|
||||
|
||||
midi.programChange = function(channel, program, delay) { // change patch (instrument)
|
||||
output.send([0xC0 + channel, program], delay * 1000);
|
||||
};
|
||||
|
||||
midi.pitchBend = function(channel, program, delay) { // pitch bend
|
||||
output.send([0xE0 + channel, program], delay * 1000);
|
||||
};
|
||||
|
||||
midi.noteOn = function(channel, note, velocity, delay) {
|
||||
output.send([0x90 + channel, note, velocity], delay * 1000);
|
||||
};
|
||||
|
||||
midi.noteOff = function(channel, note, delay) {
|
||||
output.send([0x80 + channel, note, 0], delay * 1000);
|
||||
};
|
||||
|
||||
midi.chordOn = function(channel, chord, velocity, delay) {
|
||||
for (var n = 0; n < chord.length; n ++) {
|
||||
var note = chord[n];
|
||||
output.send([0x90 + channel, note, velocity], delay * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
midi.chordOff = function(channel, chord, delay) {
|
||||
for (var n = 0; n < chord.length; n ++) {
|
||||
var note = chord[n];
|
||||
output.send([0x80 + channel, note, 0], delay * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
midi.stopAllNotes = function() {
|
||||
output.cancel();
|
||||
for (var channel = 0; channel < 16; channel ++) {
|
||||
output.send([0xB0 + channel, 0x7B, 0]);
|
||||
}
|
||||
};
|
||||
|
||||
midi.connect = function(opts) {
|
||||
root.setDefaultPlugin(midi);
|
||||
var errFunction = function(err) { // well at least we tried!
|
||||
if (window.AudioContext) { // Chrome
|
||||
opts.api = 'webaudio';
|
||||
} else if (window.Audio) { // Firefox
|
||||
opts.api = 'audiotag';
|
||||
} else { // no support
|
||||
return;
|
||||
}
|
||||
root.loadPlugin(opts);
|
||||
};
|
||||
///
|
||||
navigator.requestMIDIAccess().then(function(access) {
|
||||
plugin = access;
|
||||
var pluginOutputs = plugin.outputs;
|
||||
if (typeof pluginOutputs == 'function') { // Chrome pre-43
|
||||
output = pluginOutputs()[0];
|
||||
} else { // Chrome post-43
|
||||
output = pluginOutputs[0];
|
||||
}
|
||||
if (output === undefined) { // nothing there...
|
||||
errFunction();
|
||||
} else {
|
||||
opts.onsuccess && opts.onsuccess();
|
||||
}
|
||||
}, errFunction);
|
||||
};
|
||||
|
||||
})(MIDI);
|
@ -1,320 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
MIDI.Synesthesia : 0.3.1 : 2012-01-06
|
||||
----------------------------------------------------------
|
||||
Peacock: “Instruments to perform color-music: Two centuries of technological experimentation,” Leonardo, 21 (1988), 397-406.
|
||||
Gerstner: Karl Gerstner, The Forms of Color 1986
|
||||
Klein: Colour-Music: The art of light, London: Crosby Lockwood and Son, 1927.
|
||||
Jameson: “Visual music in a visual programming language,” IEEE Symposium on Visual Languages, 1999, 111-118.
|
||||
Helmholtz: Treatise on Physiological Optics, New York: Dover Books, 1962
|
||||
Jones: The art of light & color, New York: Van Nostrand Reinhold, 1972
|
||||
----------------------------------------------------------
|
||||
Reference: http://rhythmiclight.com/archives/ideas/colorscales.html
|
||||
----------------------------------------------------------
|
||||
*/
|
||||
|
||||
if (typeof MIDI === 'undefined') var MIDI = {};
|
||||
|
||||
MIDI.Synesthesia = MIDI.Synesthesia || {};
|
||||
|
||||
(function(root) {
|
||||
root.data = {
|
||||
'Isaac Newton (1704)': {
|
||||
format: 'HSL',
|
||||
ref: 'Gerstner, p.167',
|
||||
english: ['red',null,'orange',null,'yellow','green',null,'blue',null,'indigo',null,'violet'],
|
||||
0: [ 0, 96, 51 ], // C
|
||||
1: [ 0, 0, 0 ], // C#
|
||||
2: [ 29, 94, 52 ], // D
|
||||
3: [ 0, 0, 0 ], // D#
|
||||
4: [ 60, 90, 60 ], // E
|
||||
5: [ 135, 76, 32 ], // F
|
||||
6: [ 0, 0, 0 ], // F#
|
||||
7: [ 248, 82, 28 ], // G
|
||||
8: [ 0, 0, 0 ], // G#
|
||||
9: [ 302, 88, 26 ], // A
|
||||
10: [ 0, 0, 0 ], // A#
|
||||
11: [ 325, 84, 46 ] // B
|
||||
},
|
||||
'Louis Bertrand Castel (1734)': {
|
||||
format: 'HSL',
|
||||
ref: 'Peacock, p.400',
|
||||
english: ['blue','blue-green','green','olive green','yellow','yellow-orange','orange','red','crimson','violet','agate','indigo'],
|
||||
0: [ 248, 82, 28 ],
|
||||
1: [ 172, 68, 34 ],
|
||||
2: [ 135, 76, 32 ],
|
||||
3: [ 79, 59, 36 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 49, 90, 60 ],
|
||||
6: [ 29, 94, 52 ],
|
||||
7: [ 360, 96, 51 ],
|
||||
8: [ 1, 89, 33 ],
|
||||
9: [ 325, 84, 46 ],
|
||||
10: [ 273, 80, 27 ],
|
||||
11: [ 302, 88, 26 ]
|
||||
},
|
||||
'George Field (1816)': {
|
||||
format: 'HSL',
|
||||
ref: 'Klein, p.69',
|
||||
english: ['blue',null,'purple',null,'red','orange',null,'yellow',null,'yellow green',null,'green'],
|
||||
0: [ 248, 82, 28 ],
|
||||
1: [ 0, 0, 0 ],
|
||||
2: [ 302, 88, 26 ],
|
||||
3: [ 0, 0, 0 ],
|
||||
4: [ 360, 96, 51 ],
|
||||
5: [ 29, 94, 52 ],
|
||||
6: [ 0, 0, 0 ],
|
||||
7: [ 60, 90, 60 ],
|
||||
8: [ 0, 0, 0 ],
|
||||
9: [ 79, 59, 36 ],
|
||||
10: [ 0, 0, 0 ],
|
||||
11: [ 135, 76, 32 ]
|
||||
},
|
||||
'D. D. Jameson (1844)': {
|
||||
format: 'HSL',
|
||||
ref: 'Jameson, p.12',
|
||||
english: ['red','red-orange','orange','orange-yellow','yellow','green','green-blue','blue','blue-purple','purple','purple-violet','violet'],
|
||||
0: [ 360, 96, 51 ],
|
||||
1: [ 14, 91, 51 ],
|
||||
2: [ 29, 94, 52 ],
|
||||
3: [ 49, 90, 60 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 135, 76, 32 ],
|
||||
6: [ 172, 68, 34 ],
|
||||
7: [ 248, 82, 28 ],
|
||||
8: [ 273, 80, 27 ],
|
||||
9: [ 302, 88, 26 ],
|
||||
10: [ 313, 78, 37 ],
|
||||
11: [ 325, 84, 46 ]
|
||||
},
|
||||
'Theodor Seemann (1881)': {
|
||||
format: 'HSL',
|
||||
ref: 'Klein, p.86',
|
||||
english: ['carmine','scarlet','orange','yellow-orange','yellow','green','green blue','blue','indigo','violet','brown','black'],
|
||||
0: [ 0, 58, 26 ],
|
||||
1: [ 360, 96, 51 ],
|
||||
2: [ 29, 94, 52 ],
|
||||
3: [ 49, 90, 60 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 135, 76, 32 ],
|
||||
6: [ 172, 68, 34 ],
|
||||
7: [ 248, 82, 28 ],
|
||||
8: [ 302, 88, 26 ],
|
||||
9: [ 325, 84, 46 ],
|
||||
10: [ 0, 58, 26 ],
|
||||
11: [ 0, 0, 3 ]
|
||||
},
|
||||
'A. Wallace Rimington (1893)': {
|
||||
format: 'HSL',
|
||||
ref: 'Peacock, p.402',
|
||||
english: ['deep red','crimson','orange-crimson','orange','yellow','yellow-green','green','blueish green','blue-green','indigo','deep blue','violet'],
|
||||
0: [ 360, 96, 51 ],
|
||||
1: [ 1, 89, 33 ],
|
||||
2: [ 14, 91, 51 ],
|
||||
3: [ 29, 94, 52 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 79, 59, 36 ],
|
||||
6: [ 135, 76, 32 ],
|
||||
7: [ 163, 62, 40 ],
|
||||
8: [ 172, 68, 34 ],
|
||||
9: [ 302, 88, 26 ],
|
||||
10: [ 248, 82, 28 ],
|
||||
11: [ 325, 84, 46 ]
|
||||
},
|
||||
'Bainbridge Bishop (1893)': {
|
||||
format: 'HSL',
|
||||
ref: 'Bishop, p.11',
|
||||
english: ['red','orange-red or scarlet','orange','gold or yellow-orange','yellow or green-gold','yellow-green','green','greenish-blue or aquamarine','blue','indigo or violet-blue','violet','violet-red','red'],
|
||||
0: [ 360, 96, 51 ],
|
||||
1: [ 1, 89, 33 ],
|
||||
2: [ 29, 94, 52 ],
|
||||
3: [ 50, 93, 52 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 73, 73, 55 ],
|
||||
6: [ 135, 76, 32 ],
|
||||
7: [ 163, 62, 40 ],
|
||||
8: [ 302, 88, 26 ],
|
||||
9: [ 325, 84, 46 ],
|
||||
10: [ 343, 79, 47 ],
|
||||
11: [ 360, 96, 51 ]
|
||||
},
|
||||
'H. von Helmholtz (1910)': {
|
||||
format: 'HSL',
|
||||
ref: 'Helmholtz, p.22',
|
||||
english: ['yellow','green','greenish blue','cayan-blue','indigo blue','violet','end of red','red','red','red','red orange','orange'],
|
||||
0: [ 60, 90, 60 ],
|
||||
1: [ 135, 76, 32 ],
|
||||
2: [ 172, 68, 34 ],
|
||||
3: [ 211, 70, 37 ],
|
||||
4: [ 302, 88, 26 ],
|
||||
5: [ 325, 84, 46 ],
|
||||
6: [ 330, 84, 34 ],
|
||||
7: [ 360, 96, 51 ],
|
||||
8: [ 10, 91, 43 ],
|
||||
9: [ 10, 91, 43 ],
|
||||
10: [ 8, 93, 51 ],
|
||||
11: [ 28, 89, 50 ]
|
||||
},
|
||||
'Alexander Scriabin (1911)': {
|
||||
format: 'HSL',
|
||||
ref: 'Jones, p.104',
|
||||
english: ['red','violet','yellow','steely with the glint of metal','pearly blue the shimmer of moonshine','dark red','bright blue','rosy orange','purple','green','steely with a glint of metal','pearly blue the shimmer of moonshine'],
|
||||
0: [ 360, 96, 51 ],
|
||||
1: [ 325, 84, 46 ],
|
||||
2: [ 60, 90, 60 ],
|
||||
3: [ 245, 21, 43 ],
|
||||
4: [ 211, 70, 37 ],
|
||||
5: [ 1, 89, 33 ],
|
||||
6: [ 248, 82, 28 ],
|
||||
7: [ 29, 94, 52 ],
|
||||
8: [ 302, 88, 26 ],
|
||||
9: [ 135, 76, 32 ],
|
||||
10: [ 245, 21, 43 ],
|
||||
11: [ 211, 70, 37 ]
|
||||
},
|
||||
'Adrian Bernard Klein (1930)': {
|
||||
format: 'HSL',
|
||||
ref: 'Klein, p.209',
|
||||
english: ['dark red','red','red orange','orange','yellow','yellow green','green','blue-green','blue','blue violet','violet','dark violet'],
|
||||
0: [ 0, 91, 40 ],
|
||||
1: [ 360, 96, 51 ],
|
||||
2: [ 14, 91, 51 ],
|
||||
3: [ 29, 94, 52 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 73, 73, 55 ],
|
||||
6: [ 135, 76, 32 ],
|
||||
7: [ 172, 68, 34 ],
|
||||
8: [ 248, 82, 28 ],
|
||||
9: [ 292, 70, 31 ],
|
||||
10: [ 325, 84, 46 ],
|
||||
11: [ 330, 84, 34 ]
|
||||
},
|
||||
'August Aeppli (1940)': {
|
||||
format: 'HSL',
|
||||
ref: 'Gerstner, p.169',
|
||||
english: ['red',null,'orange',null,'yellow',null,'green','blue-green',null,'ultramarine blue','violet','purple'],
|
||||
0: [ 0, 96, 51 ],
|
||||
1: [ 0, 0, 0 ],
|
||||
2: [ 29, 94, 52 ],
|
||||
3: [ 0, 0, 0 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 0, 0, 0 ],
|
||||
6: [ 135, 76, 32 ],
|
||||
7: [ 172, 68, 34 ],
|
||||
8: [ 0, 0, 0 ],
|
||||
9: [ 211, 70, 37 ],
|
||||
10: [ 273, 80, 27 ],
|
||||
11: [ 302, 88, 26 ]
|
||||
},
|
||||
'I. J. Belmont (1944)': {
|
||||
ref: 'Belmont, p.226',
|
||||
english: ['red','red-orange','orange','yellow-orange','yellow','yellow-green','green','blue-green','blue','blue-violet','violet','red-violet'],
|
||||
0: [ 360, 96, 51 ],
|
||||
1: [ 14, 91, 51 ],
|
||||
2: [ 29, 94, 52 ],
|
||||
3: [ 50, 93, 52 ],
|
||||
4: [ 60, 90, 60 ],
|
||||
5: [ 73, 73, 55 ],
|
||||
6: [ 135, 76, 32 ],
|
||||
7: [ 172, 68, 34 ],
|
||||
8: [ 248, 82, 28 ],
|
||||
9: [ 313, 78, 37 ],
|
||||
10: [ 325, 84, 46 ],
|
||||
11: [ 338, 85, 37 ]
|
||||
},
|
||||
'Steve Zieverink (2004)': {
|
||||
format: 'HSL',
|
||||
ref: 'Cincinnati Contemporary Art Center',
|
||||
english: ['yellow-green','green','blue-green','blue','indigo','violet','ultra violet','infra red','red','orange','yellow-white','yellow'],
|
||||
0: [ 73, 73, 55 ],
|
||||
1: [ 135, 76, 32 ],
|
||||
2: [ 172, 68, 34 ],
|
||||
3: [ 248, 82, 28 ],
|
||||
4: [ 302, 88, 26 ],
|
||||
5: [ 325, 84, 46 ],
|
||||
6: [ 326, 79, 24 ],
|
||||
7: [ 1, 89, 33 ],
|
||||
8: [ 360, 96, 51 ],
|
||||
9: [ 29, 94, 52 ],
|
||||
10: [ 62, 78, 74 ],
|
||||
11: [ 60, 90, 60 ]
|
||||
},
|
||||
'Circle of Fifths (Johnston 2003)': {
|
||||
format: 'RGB',
|
||||
ref: 'Joseph Johnston',
|
||||
english: ['yellow', 'blue', 'orange', 'teal', 'red', 'green', 'purple', 'light orange', 'light blue', 'dark orange', 'dark green', 'violet' ],
|
||||
0: [ 255, 255, 0 ],
|
||||
1: [ 50, 0, 255 ],
|
||||
2: [ 255, 150, 0 ],
|
||||
3: [ 0, 210, 180 ],
|
||||
4: [ 255, 0, 0 ],
|
||||
5: [ 130, 255, 0 ],
|
||||
6: [ 150, 0, 200 ],
|
||||
7: [ 255, 195, 0 ],
|
||||
8: [ 30, 130, 255 ],
|
||||
9: [ 255, 100, 0 ],
|
||||
10: [ 0, 200, 0 ],
|
||||
11: [ 225, 0, 225 ]
|
||||
},
|
||||
'Circle of Fifths (Wheatman 2002)': {
|
||||
format: 'HEX',
|
||||
ref: 'Stuart Wheatman', // http://www.valleysfamilychurch.org/
|
||||
english: [],
|
||||
data: ['#122400', '#2E002E', '#002914', '#470000', '#002142', '#2E2E00', '#290052', '#003D00', '#520029', '#003D3D', '#522900', '#000080', '#244700', '#570057', '#004D26', '#7A0000', '#003B75', '#4C4D00', '#47008F', '#006100', '#850042', '#005C5C', '#804000', '#0000C7', '#366B00', '#80007F', '#00753B', '#B80000', '#0057AD', '#6B6B00', '#6600CC', '#008A00', '#B8005C', '#007F80', '#B35900', '#2424FF', '#478F00', '#AD00AD', '#00994D', '#F00000', '#0073E6', '#8F8F00', '#8A14FF', '#00AD00', '#EB0075', '#00A3A3', '#E07000', '#6B6BFF', '#5CB800', '#DB00DB', '#00C261', '#FF5757', '#3399FF', '#ADAD00', '#B56BFF', '#00D600', '#FF57AB', '#00C7C7', '#FF9124', '#9999FF', '#6EDB00', '#FF29FF', '#00E070', '#FF9999', '#7ABDFF', '#D1D100', '#D1A3FF', '#00FA00', '#FFA3D1', '#00E5E6', '#FFC285', '#C2C2FF', '#80FF00', '#FFA8FF', '#00E070', '#FFCCCC', '#C2E0FF', '#F0F000', '#EBD6FF', '#ADFFAD', '#FFD6EB', '#8AFFFF', '#FFEBD6', '#EBEBFF', '#E0FFC2', '#FFEBFF', '#E5FFF2', '#FFF5F5'] }
|
||||
};
|
||||
|
||||
root.map = function(type) {
|
||||
var data = {};
|
||||
var blend = function(a, b) {
|
||||
return [ // blend two colors and round results
|
||||
(a[0] * 0.5 + b[0] * 0.5 + 0.5) >> 0,
|
||||
(a[1] * 0.5 + b[1] * 0.5 + 0.5) >> 0,
|
||||
(a[2] * 0.5 + b[2] * 0.5 + 0.5) >> 0
|
||||
];
|
||||
};
|
||||
///
|
||||
var syn = root.data;
|
||||
var colors = syn[type] || syn['D. D. Jameson (1844)'];
|
||||
for (var note = 0, pclr, H, S, L; note <= 88; note ++) { // creates mapping for 88 notes
|
||||
if (colors.data) {
|
||||
data[note] = {
|
||||
hsl: colors.data[note],
|
||||
hex: colors.data[note]
|
||||
};
|
||||
} else {
|
||||
var clr = colors[(note + 9) % 12];
|
||||
///
|
||||
switch(colors.format) {
|
||||
case 'RGB':
|
||||
clr = Color.Space(clr, 'RGB>HSL');
|
||||
H = clr.H >> 0;
|
||||
S = clr.S >> 0;
|
||||
L = clr.L >> 0;
|
||||
break;
|
||||
case 'HSL':
|
||||
H = clr[0];
|
||||
S = clr[1];
|
||||
L = clr[2];
|
||||
break;
|
||||
}
|
||||
///
|
||||
if (H === S && S === L) { // note color is unset
|
||||
clr = blend(pclr, colors[(note + 10) % 12]);
|
||||
}
|
||||
///
|
||||
// var amount = L / 10;
|
||||
// var octave = note / 12 >> 0;
|
||||
// var octaveLum = L + amount * octave - 3.0 * amount; // map luminance to octave
|
||||
///
|
||||
data[note] = {
|
||||
hsl: 'hsla(' + H + ',' + S + '%,' + L + '%, 1)',
|
||||
hex: Color.Space({H: H, S: S, L: L}, 'HSL>RGB>HEX>W3')
|
||||
};
|
||||
///
|
||||
pclr = clr;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
})(MIDI.Synesthesia);
|
@ -1,225 +0,0 @@
|
||||
/*
|
||||
-----------------------------------------------------------
|
||||
dom.loadScript.js : 0.1.4 : 2014/02/12 : http://mudcu.be
|
||||
-----------------------------------------------------------
|
||||
Copyright 2011-2014 Mudcube. All rights reserved.
|
||||
-----------------------------------------------------------
|
||||
/// No verification
|
||||
dom.loadScript.add("../js/jszip/jszip.js");
|
||||
/// Strict loading order and verification.
|
||||
dom.loadScript.add({
|
||||
strictOrder: true,
|
||||
urls: [
|
||||
{
|
||||
url: "../js/jszip/jszip.js",
|
||||
verify: "JSZip",
|
||||
onsuccess: function() {
|
||||
console.log(1)
|
||||
}
|
||||
},
|
||||
{
|
||||
url: "../inc/downloadify/js/swfobject.js",
|
||||
verify: "swfobject",
|
||||
onsuccess: function() {
|
||||
console.log(2)
|
||||
}
|
||||
}
|
||||
],
|
||||
onsuccess: function() {
|
||||
console.log(3)
|
||||
}
|
||||
});
|
||||
/// Just verification.
|
||||
dom.loadScript.add({
|
||||
url: "../js/jszip/jszip.js",
|
||||
verify: "JSZip",
|
||||
onsuccess: function() {
|
||||
console.log(1)
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
if (typeof(dom) === "undefined") var dom = {};
|
||||
|
||||
(function() { "use strict";
|
||||
|
||||
dom.loadScript = function() {
|
||||
this.loaded = {};
|
||||
this.loading = {};
|
||||
return this;
|
||||
};
|
||||
|
||||
dom.loadScript.prototype.add = function(config) {
|
||||
var that = this;
|
||||
if (typeof(config) === "string") {
|
||||
config = { url: config };
|
||||
}
|
||||
var urls = config.urls;
|
||||
if (typeof(urls) === "undefined") {
|
||||
urls = [{
|
||||
url: config.url,
|
||||
verify: config.verify
|
||||
}];
|
||||
}
|
||||
/// adding the elements to the head
|
||||
var doc = document.getElementsByTagName("head")[0];
|
||||
///
|
||||
var testElement = function(element, test) {
|
||||
if (that.loaded[element.url]) return;
|
||||
if (test && globalExists(test) === false) return;
|
||||
that.loaded[element.url] = true;
|
||||
//
|
||||
if (that.loading[element.url]) that.loading[element.url]();
|
||||
delete that.loading[element.url];
|
||||
//
|
||||
if (element.onsuccess) element.onsuccess();
|
||||
if (typeof(getNext) !== "undefined") getNext();
|
||||
};
|
||||
///
|
||||
var hasError = false;
|
||||
var batchTest = [];
|
||||
var addElement = function(element) {
|
||||
if (typeof(element) === "string") {
|
||||
element = {
|
||||
url: element,
|
||||
verify: config.verify
|
||||
};
|
||||
}
|
||||
if (/([\w\d.\[\]\'\"])$/.test(element.verify)) { // check whether its a variable reference
|
||||
var verify = element.test = element.verify;
|
||||
if (typeof(verify) === "object") {
|
||||
for (var n = 0; n < verify.length; n ++) {
|
||||
batchTest.push(verify[n]);
|
||||
}
|
||||
} else {
|
||||
batchTest.push(verify);
|
||||
}
|
||||
}
|
||||
if (that.loaded[element.url]) return;
|
||||
var script = document.createElement("script");
|
||||
script.onreadystatechange = function() {
|
||||
if (this.readyState !== "loaded" && this.readyState !== "complete") return;
|
||||
testElement(element);
|
||||
};
|
||||
script.onload = function() {
|
||||
testElement(element);
|
||||
};
|
||||
script.onerror = function() {
|
||||
hasError = true;
|
||||
delete that.loading[element.url];
|
||||
if (typeof(element.test) === "object") {
|
||||
for (var key in element.test) {
|
||||
removeTest(element.test[key]);
|
||||
}
|
||||
} else {
|
||||
removeTest(element.test);
|
||||
}
|
||||
};
|
||||
script.setAttribute("type", "text/javascript");
|
||||
script.setAttribute("src", element.url);
|
||||
doc.appendChild(script);
|
||||
that.loading[element.url] = function() {};
|
||||
};
|
||||
/// checking to see whether everything loaded properly
|
||||
var removeTest = function(test) {
|
||||
var ret = [];
|
||||
for (var n = 0; n < batchTest.length; n ++) {
|
||||
if (batchTest[n] === test) continue;
|
||||
ret.push(batchTest[n]);
|
||||
}
|
||||
batchTest = ret;
|
||||
};
|
||||
var onLoad = function(element) {
|
||||
if (element) {
|
||||
testElement(element, element.test);
|
||||
} else {
|
||||
for (var n = 0; n < urls.length; n ++) {
|
||||
testElement(urls[n], urls[n].test);
|
||||
}
|
||||
}
|
||||
var istrue = true;
|
||||
for (var n = 0; n < batchTest.length; n ++) {
|
||||
if (globalExists(batchTest[n]) === false) {
|
||||
istrue = false;
|
||||
}
|
||||
}
|
||||
if (!config.strictOrder && istrue) { // finished loading all the requested scripts
|
||||
if (hasError) {
|
||||
if (config.error) {
|
||||
config.error();
|
||||
}
|
||||
} else if (config.onsuccess) {
|
||||
config.onsuccess();
|
||||
}
|
||||
} else { // keep calling back the function
|
||||
setTimeout(function() { //- should get slower over time?
|
||||
onLoad(element);
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
/// loading methods; strict ordering or loose ordering
|
||||
if (config.strictOrder) {
|
||||
var ID = -1;
|
||||
var getNext = function() {
|
||||
ID ++;
|
||||
if (!urls[ID]) { // all elements are loaded
|
||||
if (hasError) {
|
||||
if (config.error) {
|
||||
config.error();
|
||||
}
|
||||
} else if (config.onsuccess) {
|
||||
config.onsuccess();
|
||||
}
|
||||
} else { // loading new script
|
||||
var element = urls[ID];
|
||||
var url = element.url;
|
||||
if (that.loading[url]) { // already loading from another call (attach to event)
|
||||
that.loading[url] = function() {
|
||||
if (element.onsuccess) element.onsuccess();
|
||||
getNext();
|
||||
}
|
||||
} else if (!that.loaded[url]) { // create script element
|
||||
addElement(element);
|
||||
onLoad(element);
|
||||
} else { // it's already been successfully loaded
|
||||
getNext();
|
||||
}
|
||||
}
|
||||
};
|
||||
getNext();
|
||||
} else { // loose ordering
|
||||
for (var ID = 0; ID < urls.length; ID ++) {
|
||||
addElement(urls[ID]);
|
||||
onLoad(urls[ID]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dom.loadScript = new dom.loadScript();
|
||||
|
||||
var globalExists = function(path, root) {
|
||||
try {
|
||||
path = path.split('"').join('').split("'").join('').split(']').join('').split('[').join('.');
|
||||
var parts = path.split(".");
|
||||
var length = parts.length;
|
||||
var object = root || window;
|
||||
for (var n = 0; n < length; n ++) {
|
||||
var key = parts[n];
|
||||
if (object[key] == null) {
|
||||
return false;
|
||||
} else { //
|
||||
object = object[key];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
/// For NodeJS
|
||||
if (typeof (module) !== "undefined" && module.exports) {
|
||||
module.exports = dom.loadScript;
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
/*
|
||||
----------------------------------------------------------
|
||||
util/Request : 0.1.1 : 2015-03-26
|
||||
----------------------------------------------------------
|
||||
util.request({
|
||||
url: './dir/something.extension',
|
||||
data: 'test!',
|
||||
format: 'text', // text | xml | json | binary
|
||||
responseType: 'text', // arraybuffer | blob | document | json | text
|
||||
headers: {},
|
||||
withCredentials: true, // true | false
|
||||
///
|
||||
onerror: function(evt, percent) {
|
||||
console.log(evt);
|
||||
},
|
||||
onsuccess: function(evt, responseText) {
|
||||
console.log(responseText);
|
||||
},
|
||||
onprogress: function(evt, percent) {
|
||||
percent = Math.round(percent * 100);
|
||||
loader.create('thread', 'loading... ', percent);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
if (typeof MIDI === 'undefined') MIDI = {};
|
||||
|
||||
(function(root) {
|
||||
|
||||
var util = root.util || (root.util = {});
|
||||
|
||||
util.request = function(opts, onsuccess, onerror, onprogress) { 'use strict';
|
||||
if (typeof opts === 'string') opts = {url: opts};
|
||||
///
|
||||
var data = opts.data;
|
||||
var url = opts.url;
|
||||
var method = opts.method || (opts.data ? 'POST' : 'GET');
|
||||
var format = opts.format;
|
||||
var headers = opts.headers;
|
||||
var responseType = opts.responseType;
|
||||
var withCredentials = opts.withCredentials || false;
|
||||
///
|
||||
var onsuccess = onsuccess || opts.onsuccess;
|
||||
var onerror = onerror || opts.onerror;
|
||||
var onprogress = onprogress || opts.onprogress;
|
||||
///
|
||||
if (typeof NodeFS !== 'undefined' && root.loc.isLocalUrl(url)) {
|
||||
NodeFS.readFile(url, 'utf8', function(err, res) {
|
||||
if (err) {
|
||||
onerror && onerror(err);
|
||||
} else {
|
||||
onsuccess && onsuccess({responseText: res});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
///
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url, true);
|
||||
///
|
||||
if (headers) {
|
||||
for (var type in headers) {
|
||||
xhr.setRequestHeader(type, headers[type]);
|
||||
}
|
||||
} else if (data) { // set the default headers for POST
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
if (format === 'binary') { //- default to responseType="blob" when supported
|
||||
if (xhr.overrideMimeType) {
|
||||
xhr.overrideMimeType('text/plain; charset=x-user-defined');
|
||||
}
|
||||
}
|
||||
if (responseType) {
|
||||
xhr.responseType = responseType;
|
||||
}
|
||||
if (withCredentials) {
|
||||
xhr.withCredentials = 'true';
|
||||
}
|
||||
if (onerror && 'onerror' in xhr) {
|
||||
xhr.onerror = onerror;
|
||||
}
|
||||
if (onprogress && xhr.upload && 'onprogress' in xhr.upload) {
|
||||
if (data) {
|
||||
xhr.upload.onprogress = function(evt) {
|
||||
onprogress.call(xhr, evt, event.loaded / event.total);
|
||||
};
|
||||
} else {
|
||||
xhr.addEventListener('progress', function(evt) {
|
||||
var totalBytes = 0;
|
||||
if (evt.lengthComputable) {
|
||||
totalBytes = evt.total;
|
||||
} else if (xhr.totalBytes) {
|
||||
totalBytes = xhr.totalBytes;
|
||||
} else {
|
||||
var rawBytes = parseInt(xhr.getResponseHeader('Content-Length-Raw'));
|
||||
if (isFinite(rawBytes)) {
|
||||
xhr.totalBytes = totalBytes = rawBytes;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
onprogress.call(xhr, evt, evt.loaded / totalBytes);
|
||||
});
|
||||
}
|
||||
}
|
||||
///
|
||||
xhr.onreadystatechange = function(evt) {
|
||||
if (xhr.readyState === 4) { // The request is complete
|
||||
if (xhr.status === 200 || // Response OK
|
||||
xhr.status === 304 || // Not Modified
|
||||
xhr.status === 308 || // Permanent Redirect
|
||||
xhr.status === 0 && root.client.cordova // Cordova quirk
|
||||
) {
|
||||
if (onsuccess) {
|
||||
var res;
|
||||
if (format === 'xml') {
|
||||
res = evt.target.responseXML;
|
||||
} else if (format === 'text') {
|
||||
res = evt.target.responseText;
|
||||
} else if (format === 'json') {
|
||||
try {
|
||||
res = JSON.parse(evt.target.response);
|
||||
} catch(err) {
|
||||
onerror && onerror.call(xhr, evt);
|
||||
}
|
||||
}
|
||||
///
|
||||
onsuccess.call(xhr, evt, res);
|
||||
}
|
||||
} else {
|
||||
onerror && onerror.call(xhr, evt);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(data);
|
||||
return xhr;
|
||||
};
|
||||
|
||||
/// NodeJS
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
var NodeFS = require('fs');
|
||||
XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
|
||||
module.exports = root.util.request;
|
||||
}
|
||||
|
||||
})(MIDI);
|
@ -1,70 +0,0 @@
|
||||
var midiLoaded = false;
|
||||
function loadMIDI() {
|
||||
MIDI.loadPlugin({
|
||||
soundfontUrl: "/static/trading-in-the-rain/soundfont/",
|
||||
instrument: "acoustic_grand_piano",
|
||||
onprogress: (state, progress) => {
|
||||
console.log("MIDI loading...", progress*100, "%");
|
||||
},
|
||||
onsuccess: () => {
|
||||
console.log("MIDI is ready to be used");
|
||||
MIDI.setVolume(0, 127);
|
||||
midiLoaded = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function MusicBox(priceDist, volumeDist) {
|
||||
this.priceDist = priceDist;
|
||||
this.volumeDist = volumeDist;
|
||||
|
||||
// clamp the keyboard so we're not using the very low notes, they don't sound
|
||||
// good.
|
||||
const noteRange = {
|
||||
//low: 21,
|
||||
//low: 36, // C2
|
||||
low: 60, // C4, middle C
|
||||
high: 108
|
||||
};
|
||||
|
||||
|
||||
function makeScale(tpl) {
|
||||
tplObj = {};
|
||||
for (i in tpl) {
|
||||
tplObj[tpl[i]] = true;
|
||||
}
|
||||
|
||||
let scale = [];
|
||||
for (let note=noteRange.low; note<=noteRange.high; note++) {
|
||||
let key = MIDI.noteToKey[note].replace(/\d+$/, "");
|
||||
if (tplObj[key]) {
|
||||
scale.push(note);
|
||||
}
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
//this.scale = makeScale(["C", "D", "E", "F", "G", "A", "B"]); // cMajor
|
||||
//this.scale = makeScale(["D", "E", "Gb", "G", "A", "Db"]); //dMajor
|
||||
//this.scale = makeScale(["C", "D", "E", "G", "A"]); // cMajor pentatonic
|
||||
this.scale = makeScale(["F", "G", "A", "C", "D"]); // fMajor pentatonic
|
||||
|
||||
this.playNote = (note, holdFor) => {
|
||||
if (!midiLoaded) return;
|
||||
let velocity = 127;
|
||||
MIDI.noteOn(0, note, velocity, 0);
|
||||
MIDI.noteOff(0, note, holdFor);
|
||||
};
|
||||
|
||||
this.playTrades = (trades) => {
|
||||
if (this.priceDist.length == 0) return;
|
||||
for (let i in trades) {
|
||||
let noteIdx = this.priceDist.distribute(trades[i].price, 0, this.scale.length-1);
|
||||
noteIdx = Math.round(noteIdx);
|
||||
|
||||
let holdFor = 0.25 + this.volumeDist.distribute(trades[i].volume, 0, 1.75);
|
||||
let note = this.scale[noteIdx];
|
||||
this.playNote(note, holdFor);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
function RainCanvas(canvasDOM) {
|
||||
this.canvas = canvasDOM;
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
|
||||
this.drops = [];
|
||||
this.tick = 0;
|
||||
|
||||
// drop: {x, y, intensity, color} (all in range [0, 1], except color which is
|
||||
// an array [r,g,b])
|
||||
this.newDrop = (newDrop) => {
|
||||
if (!document.hasFocus()) return;
|
||||
|
||||
// scale intensity up a bit right off the bat. If the intensity was near 0
|
||||
// the drop wouldn't actually show up at all.
|
||||
newDrop.intensity = distribute(newDrop.intensity, 0, 1, 0.1, 1);
|
||||
newDrop.tick = this.tick;
|
||||
this.drops.push(newDrop);
|
||||
};
|
||||
|
||||
// alpha isn't really alpha, it's used to determine line width, but it plays
|
||||
// the same role.
|
||||
this.drawDrop = (drop, alpha) => {
|
||||
let cW = this.canvas.width, cH = this.canvas.height;
|
||||
let minDim = Math.min(cW, cH);
|
||||
|
||||
let tickDiff = this.tick - drop.tick;
|
||||
let radius = tickDiff * (minDim / 250);
|
||||
let x = distribute(drop.x, 0, 1, cW*0.1, cW*0.9);
|
||||
let y = distribute(drop.y, 0, 1, cH*0.1, cH*0.9);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, radius, 0, Math.PI * 2, false);
|
||||
this.ctx.closePath();
|
||||
|
||||
// multiple lineWidth by alpha so that the line width drops over time in
|
||||
// correspondence with the opacity.
|
||||
this.ctx.lineWidth = distribute(drop.intensity, 0, 1, 2, 9) * alpha;
|
||||
|
||||
let r = drop.color[0], g = drop.color[1], b = drop.color[2];
|
||||
this.ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 1)`;
|
||||
this.ctx.stroke();
|
||||
};
|
||||
|
||||
let requestAnimationFrame =
|
||||
window.requestAnimationFrame ||
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
window.msRequestAnimationFrame;
|
||||
|
||||
this.doTick = () => {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
let newDrops = [];
|
||||
for (let i in this.drops) {
|
||||
let drop = this.drops[i];
|
||||
let alpha = distribute(
|
||||
this.tick - drop.tick,
|
||||
0, 200 * drop.intensity,
|
||||
1, 0,
|
||||
);
|
||||
if (alpha <= 0) continue;
|
||||
|
||||
this.drawDrop(drop, alpha);
|
||||
newDrops.push(drop);
|
||||
}
|
||||
this.drops = newDrops;
|
||||
|
||||
this.tick++;
|
||||
requestAnimationFrame(this.doTick);
|
||||
};
|
||||
requestAnimationFrame(this.doTick);
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
function SeriesComposer(resource, rainCanvas, color) {
|
||||
this.rainCanvas = rainCanvas;
|
||||
this.color = color;
|
||||
|
||||
this.priceDist = new Distributor(200);
|
||||
this.volumeDist = new Distributor(200);
|
||||
this.musicBox = new MusicBox(this.priceDist, this.volumeDist);
|
||||
|
||||
this.enabled = false;
|
||||
this.setEnabled = (enabled) => this.enabled = enabled;
|
||||
this.getEnabled = () => { return this.enabled; }
|
||||
|
||||
this.totalTrades = 0;
|
||||
this.getTotalTrades = () => { return this.totalTrades; }
|
||||
|
||||
this.cw = new CW(resource);
|
||||
this.cw.ontrades = (trades) => {
|
||||
if (this.totalTrades > 0 && this.enabled) {
|
||||
let priceVols = {}; // sum volumes by price, for deduplication
|
||||
for (let i in trades) {
|
||||
let price = trades[i].price, volume = trades[i].volume;
|
||||
if (!priceVols[price]) priceVols[price] = 0;
|
||||
priceVols[price] += volume;
|
||||
}
|
||||
|
||||
trades = []; // overwrite trades with deduplicated ones.
|
||||
for (let price in priceVols) {
|
||||
let volume = priceVols[price];
|
||||
let intensity = this.volumeDist.distribute(volume, 0, 1);
|
||||
this.rainCanvas.newDrop({
|
||||
x: this.priceDist.distribute(price, 0, 1),
|
||||
y: Math.random(),
|
||||
intensity: intensity,
|
||||
color: this.color,
|
||||
});
|
||||
|
||||
trades.push({price: price, volume: volume});
|
||||
}
|
||||
|
||||
this.musicBox.playTrades(trades);
|
||||
}
|
||||
|
||||
for (let i in trades) {
|
||||
this.priceDist.add(trades[i].price);
|
||||
this.volumeDist.add(trades[i].volume);
|
||||
}
|
||||
|
||||
this.totalTrades += trades.length;
|
||||
if (this.ontrades) this.ontrades(trades);
|
||||
};
|
||||
}
|
Binary file not shown.
@ -1,144 +0,0 @@
|
||||
const tradesToWaitFor = 5;
|
||||
|
||||
function hexToRgb(hex) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return [
|
||||
parseInt(result[1], 16),
|
||||
parseInt(result[2], 16),
|
||||
parseInt(result[3], 16),
|
||||
];
|
||||
}
|
||||
|
||||
const colorPalette = [
|
||||
"#28D2EE",
|
||||
"#ED778E",
|
||||
"#6557DC",
|
||||
"#EEE386",
|
||||
"#B55AA0",
|
||||
];
|
||||
|
||||
// Use https://api.cryptowat.ch/markets/<exchange>
|
||||
// or https://api.cryptowat.ch/pairs (for "all")
|
||||
const markets = {
|
||||
"kraken":[
|
||||
{
|
||||
name: "BTCUSD",
|
||||
resource: "markets:87:trades",
|
||||
},
|
||||
{
|
||||
name: "BTCEUR",
|
||||
resource: "markets:86:trades",
|
||||
},
|
||||
{
|
||||
name: "BTCEUR",
|
||||
resource: "markets:96:trades",
|
||||
},
|
||||
{
|
||||
name: "ETHEUR",
|
||||
resource: "markets:97:trades",
|
||||
},
|
||||
{
|
||||
name: "BCHUSD",
|
||||
resource: "markets:146:trades",
|
||||
},
|
||||
],
|
||||
"bitfinex":[
|
||||
{
|
||||
name: "BTCUSD",
|
||||
resource: "markets:1:trades",
|
||||
},
|
||||
{
|
||||
name: "ETHUSD",
|
||||
resource: "markets:4:trades",
|
||||
},
|
||||
{
|
||||
name: "BSVUSD",
|
||||
resource: "markets:5558:trades",
|
||||
},
|
||||
{
|
||||
name: "BTCEUR",
|
||||
resource: "markets:415:trades",
|
||||
},
|
||||
{
|
||||
name: "XRPUSD",
|
||||
resource: "markets:25:trades",
|
||||
},
|
||||
],
|
||||
"all": [
|
||||
{
|
||||
name: "BTCUSD",
|
||||
resource: "instruments:9:trades",
|
||||
},
|
||||
{
|
||||
name: "ETHUSD",
|
||||
resource: "instruments:125:trades",
|
||||
},
|
||||
{
|
||||
name: "LTCUSD",
|
||||
resource: "instruments:138:trades",
|
||||
},
|
||||
{
|
||||
name: "EOSUSD",
|
||||
resource: "instruments:4:trades",
|
||||
},
|
||||
{
|
||||
name: "XRPUSD",
|
||||
resource: "instruments:160:trades",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const exchange = "all";
|
||||
|
||||
function fillMarketP() {
|
||||
let marketsEl = document.getElementById("markets");
|
||||
for (let i in markets[exchange]) {
|
||||
let name = markets[exchange][i].name;
|
||||
let color = colorPalette[i];
|
||||
if (i > 0) marketsEl.innerHTML += "</br>";
|
||||
marketsEl.innerHTML += `<strong style="color: ${color}; font-size: 2rem;">${name}</strong>`;
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
document.getElementById("button").style.display = "none";
|
||||
|
||||
let progress = document.getElementById("progress");
|
||||
progress.innerHTML = "Connecting to Cryptowat.ch...";
|
||||
|
||||
let canvas = document.getElementById("rainCanvas");
|
||||
let rainCanvas = new RainCanvas(canvas);
|
||||
|
||||
let modalHidden = false;
|
||||
for (let i in markets[exchange]) {
|
||||
let seriesComposer = new SeriesComposer(
|
||||
markets[exchange][i].resource,
|
||||
rainCanvas,
|
||||
hexToRgb(colorPalette[i]),
|
||||
);
|
||||
|
||||
seriesComposer.cw.onconnect = () => {
|
||||
progress.innerHTML = "Preloading a few trades before continuing.";
|
||||
};
|
||||
|
||||
// wait for each series to rech tradesToWaitFor before letting it begin.
|
||||
// Hide the modal when the first series is enabled.
|
||||
seriesComposer.ontrades = (trades) => {
|
||||
if (!modalHidden && seriesComposer.getTotalTrades() < tradesToWaitFor) {
|
||||
progress.innerHTML += "."; // indicate that _something_ is happening
|
||||
return;
|
||||
}
|
||||
if (!modalHidden) {
|
||||
let modal = document.getElementById("tradingInRainModal");
|
||||
modal.style.display = "none";
|
||||
modalHidden = true;
|
||||
}
|
||||
seriesComposer.setEnabled(true);
|
||||
seriesComposer.ontrades = undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function autorun() {
|
||||
loadMIDI();
|
||||
}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user