diff --git a/elements/fun/music/music.go b/elements/fun/music/music.go new file mode 100644 index 0000000..badb2a3 --- /dev/null +++ b/elements/fun/music/music.go @@ -0,0 +1,69 @@ +package music + +import "math" + +var semitone = math.Pow(2, 1.0 / 12.0) + +// Tuning is an interface representing a tuning. +type Tuning interface { + // Tune returns the frequency of a given note in Hz. + Tune (Note) float64 +} + +// EqualTemparment implements twelve-tone equal temparment. +type EqualTemparment struct { A4 float64 } + +// Tune returns the EqualTemparment frequency of a given note in Hz. +func (tuning EqualTemparment) Tune (note Note) float64 { + return tuning.A4 * math.Pow(semitone, float64(note - NoteA4)) +} + +// Octave represents a MIDI octave. +type Octave int + +// Note returns the note at the specified scale degree in the chromatic scale. +func (octave Octave) Note (degree int) Note { + return Note(int(octave + 1) * 12 + degree) +} + +// Note represents a MIDI note. +type Note int + +const ( + NoteC0 Note = iota + NoteDb0 + NoteD0 + NoteEb0 + NoteE0 + NoteF0 + NoteGb0 + NoteG0 + NoteAb0 + NoteA0 + NoteBb0 + NoteB0 + + NoteA4 Note = 69 +) + +// Octave returns the octave of the note +func (note Note) Octave () int { + return int(note / 12 - 1) +} + +// Degree returns the scale degree of the note in the chromatic scale. +func (note Note) Degree () int { + mod := note % 12 + if mod < 0 { mod += 12 } + return int(mod) +} + +// IsSharp returns whether or not the note is a sharp. +func (note Note) IsSharp () bool { + degree := note.Degree() + return degree == 1 || + degree == 3 || + degree == 6 || + degree == 8 || + degree == 10 +} diff --git a/elements/fun/piano.go b/elements/fun/piano.go index 900c6a3..18740bc 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -6,51 +6,19 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" - -// Octave represents a MIDI octave. -type Octave int - -// Note returns the note at the specified scale degree in the chromatic scale. -func (octave Octave) Note (degree int) Note { - return Note(int(octave + 1) * 12 + degree) -} - -// Note represents a MIDI note. -type Note int - -// Octave returns the octave of the note -func (note Note) Octave () int { - return int(note / 12 - 1) -} - -// Degree returns the scale degree of the note in the chromatic scale. -func (note Note) Degree () int { - mod := note % 12 - if mod < 0 { mod += 12 } - return int(mod) -} - -// IsSharp returns whether or not the note is a sharp. -func (note Note) IsSharp () bool { - degree := note.Degree() - return degree == 1 || - degree == 3 || - degree == 6 || - degree == 8 || - degree == 10 -} +import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music" const pianoKeyWidth = 18 type pianoKey struct { image.Rectangle - Note + music.Note } type Piano struct { *core.Core core core.CoreControl - low, high Octave + low, high music.Octave config config.Wrapped theme theme.Wrapped @@ -60,11 +28,11 @@ type Piano struct { pressed *pianoKey - onPress func (Note) - onRelease func (Note) + onPress func (music.Note) + onRelease func (music.Note) } -func NewPiano (low, high Octave) (element *Piano) { +func NewPiano (low, high music.Octave) (element *Piano) { element = &Piano { low: low, high: high, @@ -79,12 +47,12 @@ func NewPiano (low, high Octave) (element *Piano) { } // OnPress sets a function to be called when a key is pressed. -func (element *Piano) OnPress (callback func (note Note)) { +func (element *Piano) OnPress (callback func (note music.Note)) { element.onPress = callback } // OnRelease sets a function to be called when a key is released. -func (element *Piano) OnRelease (callback func (note Note)) { +func (element *Piano) OnRelease (callback func (note music.Note)) { element.onRelease = callback } @@ -110,11 +78,6 @@ func (element *Piano) HandleMouseMove (x, y int) { func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Piano) pressUnderMouseCursor (point image.Point) { - // release previous note - if element.pressed != nil && element.onRelease != nil { - element.onRelease((*element.pressed).Note) - } - // find out which note is being pressed newKey := (*pianoKey)(nil) for index, key := range element.flatKeys { @@ -132,6 +95,11 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) { if newKey == nil { return } if newKey != element.pressed { + // release previous note + if element.pressed != nil && element.onRelease != nil { + element.onRelease((*element.pressed).Note) + } + // press new note element.pressed = newKey if element.onPress != nil { @@ -198,7 +166,7 @@ func (element *Piano) recalculate () { element.sharpKeys[sharpIndex].Rectangle = image.Rect ( -(pianoKeyWidth * 3) / 7, 0, (pianoKeyWidth * 3) / 7, - bounds.Dy() / 2).Add(dot) + (bounds.Dy() * 5) / 8).Add(dot) element.sharpKeys[sharpIndex].Note = note sharpIndex ++ } else { diff --git a/examples/piano/main.go b/examples/piano/main.go index bc74a2c..da01741 100644 --- a/examples/piano/main.go +++ b/examples/piano/main.go @@ -1,12 +1,23 @@ package main +import "github.com/faiface/beep" +import "github.com/faiface/beep/speaker" +import "github.com/faiface/beep/generators" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/fun" +import "git.tebibyte.media/sashakoshka/tomo/layouts/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic" +import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" +const sampleRate = 44100 +const bufferSize = 256 +var tuning = music.EqualTemparment { A4: 440 } + +var playing = map[music.Note] *beep.Ctrl { } + func main () { + speaker.Init(sampleRate, bufferSize) tomo.Run(run) } @@ -20,7 +31,29 @@ func run () { container.Adopt(label, false) piano := fun.NewPiano(3, 5) container.Adopt(piano, true) + piano.OnPress(playNote) + piano.OnRelease(stopNote) window.OnClose(tomo.Stop) window.Show() } + +func stopNote (note music.Note) { + if _, is := playing[note]; !is { return } + + speaker.Lock() + playing[note].Streamer = nil + delete(playing, note) + speaker.Unlock() +} + +func playNote (note music.Note) { + streamer, err := generators.SinTone(sampleRate, int(tuning.Tune(note))) + if err != nil { panic(err.Error()) } + + stopNote(note) + speaker.Lock() + playing[note] = &beep.Ctrl { Streamer: streamer } + speaker.Unlock() + speaker.Play(playing[note]) +} diff --git a/go.mod b/go.mod index 3cdf9dd..9610c45 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,19 @@ module git.tebibyte.media/sashakoshka/tomo go 1.19 require ( + github.com/faiface/beep v1.1.0 github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 golang.org/x/image v0.3.0 ) +require ( + github.com/hajimehoshi/oto v0.7.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect + golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect +) + require ( github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect diff --git a/go.sum b/go.sum index 488db99..a6a4ecf 100644 --- a/go.sum +++ b/go.sum @@ -2,25 +2,60 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= +github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= +github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= +github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= +github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= +github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= +github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk= +github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM= +github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= +github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= +github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=