The piano plays sound

This commit is contained in:
Sasha Koshka 2023-02-08 23:41:31 -05:00
parent 5c7e243566
commit c5ee7c8cdb
5 changed files with 161 additions and 47 deletions

View File

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

View File

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

View File

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

9
go.mod
View File

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

35
go.sum
View File

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