Remove leftover static assets specific to individual blog posts

This commit is contained in:
Brian Picciano 2023-08-13 22:06:32 +02:00
parent 0bc9cd83b4
commit c4ec906406
363 changed files with 0 additions and 231035 deletions

View File

@ -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
}

View File

@ -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.
//

View File

@ -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
}

View File

@ -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()
}

View File

@ -1,3 +0,0 @@
bundle:
nix-build markov.nix -A nixBundle
./result/bin/nix-bundle '((import ./markov.nix) {}).entrypoint' '/bin/markov'

View File

@ -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;
};
}

View File

@ -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();
}

View File

@ -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);
};
}

View File

@ -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.

View File

@ -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;
});
}());

View File

@ -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;
}
};

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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 = {};
};
})();

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}
};
}

View File

@ -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);
}

View File

@ -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);
};
}

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More