mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
parent
7b213bd8b0
commit
063b2c29c5
@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- Mute and solo toggles for instruments ([#168][i168])
|
||||||
- Compressor displays threshold and invgain in dB
|
- Compressor displays threshold and invgain in dB
|
||||||
- Dragging mouse to select rectangles in the tables
|
- Dragging mouse to select rectangles in the tables
|
||||||
- The standalone tracker can open a MIDI port for receiving MIDI notes
|
- The standalone tracker can open a MIDI port for receiving MIDI notes
|
||||||
@ -265,4 +266,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i158]: https://github.com/vsariola/sointu/issues/158
|
[i158]: https://github.com/vsariola/sointu/issues/158
|
||||||
[i160]: https://github.com/vsariola/sointu/issues/160
|
[i160]: https://github.com/vsariola/sointu/issues/160
|
||||||
[i162]: https://github.com/vsariola/sointu/issues/162
|
[i162]: https://github.com/vsariola/sointu/issues/162
|
||||||
[i166]: https://github.com/vsariola/sointu/issues/166
|
[i166]: https://github.com/vsariola/sointu/issues/166
|
||||||
|
[i168]: https://github.com/vsariola/sointu/issues/168
|
3
patch.go
3
patch.go
@ -18,6 +18,7 @@ type (
|
|||||||
Comment string `yaml:",omitempty"`
|
Comment string `yaml:",omitempty"`
|
||||||
NumVoices int
|
NumVoices int
|
||||||
Units []Unit
|
Units []Unit
|
||||||
|
Mute bool `yaml:",omitempty"` // Mute is only used in the tracker for soloing/muting instruments; the compiled player ignores this field
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
||||||
@ -352,7 +353,7 @@ func (instr *Instrument) Copy() Instrument {
|
|||||||
for i, u := range instr.Units {
|
for i, u := range instr.Units {
|
||||||
units[i] = u.Copy()
|
units[i] = u.Copy()
|
||||||
}
|
}
|
||||||
return Instrument{Name: instr.Name, Comment: instr.Comment, NumVoices: instr.NumVoices, Units: units}
|
return Instrument{Name: instr.Name, Comment: instr.Comment, NumVoices: instr.NumVoices, Units: units, Mute: instr.Mute}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy makes a deep copy of a Patch.
|
// Copy makes a deep copy of a Patch.
|
||||||
|
@ -22,6 +22,8 @@ type (
|
|||||||
UnitDisabled Model
|
UnitDisabled Model
|
||||||
LoopToggle Model
|
LoopToggle Model
|
||||||
UniquePatterns Model
|
UniquePatterns Model
|
||||||
|
Mute Model
|
||||||
|
Solo Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v Bool) Toggle() {
|
func (v Bool) Toggle() {
|
||||||
@ -47,6 +49,8 @@ func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m)
|
|||||||
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
|
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
|
||||||
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
|
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
|
||||||
func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) }
|
func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) }
|
||||||
|
func (m *Model) Mute() *Mute { return (*Mute)(m) }
|
||||||
|
func (m *Model) Solo() *Solo { return (*Solo)(m) }
|
||||||
|
|
||||||
// Panic methods
|
// Panic methods
|
||||||
|
|
||||||
@ -194,3 +198,53 @@ func (m *UniquePatterns) Bool() Bool { return Bool{m} }
|
|||||||
func (m *UniquePatterns) Value() bool { return m.uniquePatterns }
|
func (m *UniquePatterns) Value() bool { return m.uniquePatterns }
|
||||||
func (m *UniquePatterns) setValue(val bool) { m.uniquePatterns = val }
|
func (m *UniquePatterns) setValue(val bool) { m.uniquePatterns = val }
|
||||||
func (m *UniquePatterns) Enabled() bool { return true }
|
func (m *UniquePatterns) Enabled() bool { return true }
|
||||||
|
|
||||||
|
// Mute methods
|
||||||
|
|
||||||
|
func (m *Mute) Bool() Bool { return Bool{m} }
|
||||||
|
func (m *Mute) Value() bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[m.d.InstrIndex].Mute
|
||||||
|
}
|
||||||
|
func (m *Mute) setValue(val bool) {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("Mute", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Mute = val
|
||||||
|
}
|
||||||
|
func (m *Mute) Enabled() bool { return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) }
|
||||||
|
|
||||||
|
// Solo methods
|
||||||
|
|
||||||
|
func (m *Solo) Bool() Bool { return Bool{m} }
|
||||||
|
func (m *Solo) Value() bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range m.d.Song.Patch {
|
||||||
|
if i == m.d.InstrIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !m.d.Song.Patch[i].Mute {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !m.d.Song.Patch[m.d.InstrIndex].Mute
|
||||||
|
}
|
||||||
|
func (m *Solo) setValue(val bool) {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("Solo", PatchChange, MinorChange)()
|
||||||
|
for i := range m.d.Song.Patch {
|
||||||
|
if i == m.d.InstrIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[i].Mute = val
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Mute = false
|
||||||
|
}
|
||||||
|
func (m *Solo) Enabled() bool { return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) }
|
||||||
|
@ -33,6 +33,8 @@ type InstrumentEditor struct {
|
|||||||
addUnitBtn *ActionClickable
|
addUnitBtn *ActionClickable
|
||||||
presetMenuBtn *TipClickable
|
presetMenuBtn *TipClickable
|
||||||
commentExpandBtn *BoolClickable
|
commentExpandBtn *BoolClickable
|
||||||
|
soloBtn *BoolClickable
|
||||||
|
muteBtn *BoolClickable
|
||||||
commentEditor *Editor
|
commentEditor *Editor
|
||||||
commentString tracker.String
|
commentString tracker.String
|
||||||
nameEditor *Editor
|
nameEditor *Editor
|
||||||
@ -51,6 +53,8 @@ type InstrumentEditor struct {
|
|||||||
expandCommentHint string
|
expandCommentHint string
|
||||||
collapseCommentHint string
|
collapseCommentHint string
|
||||||
deleteInstrumentHint string
|
deleteInstrumentHint string
|
||||||
|
muteHint, unmuteHint string
|
||||||
|
soloHint, unsoloHint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||||
@ -64,6 +68,8 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
|||||||
addUnitBtn: NewActionClickable(model.AddUnit(false)),
|
addUnitBtn: NewActionClickable(model.AddUnit(false)),
|
||||||
commentExpandBtn: NewBoolClickable(model.CommentExpanded().Bool()),
|
commentExpandBtn: NewBoolClickable(model.CommentExpanded().Bool()),
|
||||||
presetMenuBtn: new(TipClickable),
|
presetMenuBtn: new(TipClickable),
|
||||||
|
soloBtn: NewBoolClickable(model.Solo().Bool()),
|
||||||
|
muteBtn: NewBoolClickable(model.Mute().Bool()),
|
||||||
commentEditor: NewEditor(widget.Editor{}),
|
commentEditor: NewEditor(widget.Editor{}),
|
||||||
nameEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle}),
|
nameEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle}),
|
||||||
searchEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start}),
|
searchEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start}),
|
||||||
@ -85,6 +91,10 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
|||||||
ret.expandCommentHint = makeHint("Expand comment", " (%s)", "CommentExpandedToggle")
|
ret.expandCommentHint = makeHint("Expand comment", " (%s)", "CommentExpandedToggle")
|
||||||
ret.collapseCommentHint = makeHint("Collapse comment", " (%s)", "CommentExpandedToggle")
|
ret.collapseCommentHint = makeHint("Collapse comment", " (%s)", "CommentExpandedToggle")
|
||||||
ret.deleteInstrumentHint = makeHint("Delete\ninstrument", "\n(%s)", "DeleteInstrument")
|
ret.deleteInstrumentHint = makeHint("Delete\ninstrument", "\n(%s)", "DeleteInstrument")
|
||||||
|
ret.muteHint = makeHint("Mute", " (%s)", "MuteToggle")
|
||||||
|
ret.unmuteHint = makeHint("Unmute", " (%s)", "MuteToggle")
|
||||||
|
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
||||||
|
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +173,8 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
|||||||
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||||
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||||
deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint)
|
deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint)
|
||||||
|
soloBtnStyle := ToggleIcon(gtx, t.Theme, ie.soloBtn, icons.SocialGroup, icons.SocialPerson, ie.soloHint, ie.unsoloHint)
|
||||||
|
muteBtnStyle := ToggleIcon(gtx, t.Theme, ie.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, ie.muteHint, ie.unmuteHint)
|
||||||
|
|
||||||
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)
|
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)
|
||||||
|
|
||||||
@ -199,6 +211,8 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
|||||||
}),
|
}),
|
||||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||||
|
layout.Rigid(soloBtnStyle.Layout),
|
||||||
|
layout.Rigid(muteBtnStyle.Layout),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||||
dims := presetMenuBtnStyle.Layout(gtx)
|
dims := presetMenuBtnStyle.Layout(gtx)
|
||||||
@ -249,7 +263,7 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
|
|||||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||||
grabhandle := LabelStyle{Text: strconv.Itoa(i + 1), ShadeColor: black, Color: mediumEmphasisTextColor, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.Theme.Shaper}
|
grabhandle := LabelStyle{Text: strconv.Itoa(i + 1), ShadeColor: black, Color: mediumEmphasisTextColor, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.Theme.Shaper}
|
||||||
label := func(gtx C) D {
|
label := func(gtx C) D {
|
||||||
name, level, ok := (*tracker.Instruments)(t.Model).Item(i)
|
name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i)
|
||||||
if !ok {
|
if !ok {
|
||||||
labelStyle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
labelStyle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||||
@ -266,6 +280,10 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
|
|||||||
style.HintColor = instrumentNameHintColor
|
style.HintColor = instrumentNameHintColor
|
||||||
style.TextSize = unit.Sp(12)
|
style.TextSize = unit.Sp(12)
|
||||||
style.Font = labelDefaultFont
|
style.Font = labelDefaultFont
|
||||||
|
if mute {
|
||||||
|
style.Color = disabledTextColor
|
||||||
|
style.Font.Style = font.Italic
|
||||||
|
}
|
||||||
dims := layout.Center.Layout(gtx, func(gtx C) D {
|
dims := layout.Center.Layout(gtx, func(gtx C) D {
|
||||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||||
return style.Layout(gtx)
|
return style.Layout(gtx)
|
||||||
@ -277,6 +295,10 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
|
|||||||
name = "Instr"
|
name = "Instr"
|
||||||
}
|
}
|
||||||
labelStyle := LabelStyle{Text: name, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
labelStyle := LabelStyle{Text: name, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||||
|
if mute {
|
||||||
|
labelStyle.Color = disabledTextColor
|
||||||
|
labelStyle.Font.Style = font.Italic
|
||||||
|
}
|
||||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||||
}
|
}
|
||||||
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6), Top: unit.Dp(4)}.Layout(gtx, func(gtx C) D {
|
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6), Top: unit.Dp(4)}.Layout(gtx, func(gtx C) D {
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
- {key: "L", shortcut: true, action: "LoopToggle"}
|
- {key: "L", shortcut: true, action: "LoopToggle"}
|
||||||
- {key: "N", shortcut: true, action: "NewSong"}
|
- {key: "N", shortcut: true, action: "NewSong"}
|
||||||
- {key: "S", shortcut: true, action: "SaveSong"}
|
- {key: "S", shortcut: true, action: "SaveSong"}
|
||||||
|
- {key: "M", shortcut: true, action: "MuteToggle"}
|
||||||
|
- {key: ",", shortcut: true, action: "SoloToggle"}
|
||||||
- {key: "O", shortcut: true, action: "OpenSong"}
|
- {key: "O", shortcut: true, action: "OpenSong"}
|
||||||
- {key: "I", shortcut: true, shift: true, action: "DeleteInstrument"}
|
- {key: "I", shortcut: true, shift: true, action: "DeleteInstrument"}
|
||||||
- {key: "I", shortcut: true, action: "AddInstrument"}
|
- {key: "I", shortcut: true, action: "AddInstrument"}
|
||||||
|
@ -222,6 +222,10 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
t.LoopToggle().Bool().Toggle()
|
t.LoopToggle().Bool().Toggle()
|
||||||
case "UniquePatternsToggle":
|
case "UniquePatternsToggle":
|
||||||
t.UniquePatterns().Bool().Toggle()
|
t.UniquePatterns().Bool().Toggle()
|
||||||
|
case "MuteToggle":
|
||||||
|
t.Mute().Bool().Toggle()
|
||||||
|
case "SoloToggle":
|
||||||
|
t.Solo().Bool().Toggle()
|
||||||
// Integers
|
// Integers
|
||||||
case "InstrumentVoicesAdd":
|
case "InstrumentVoicesAdd":
|
||||||
t.Model.InstrumentVoices().Int().Add(1)
|
t.Model.InstrumentVoices().Int().Add(1)
|
||||||
|
@ -319,7 +319,7 @@ func (p ParameterStyle) Layout(gtx C) D {
|
|||||||
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
||||||
for i := range instrItems {
|
for i := range instrItems {
|
||||||
i := i
|
i := i
|
||||||
name, _, _ := p.tracker.Instruments().Item(i)
|
name, _, _, _ := p.tracker.Instruments().Item(i)
|
||||||
instrItems[i].Text = name
|
instrItems[i].Text = name
|
||||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||||
instrItems[i].Doer = tracker.Allow(func() {
|
instrItems[i].Doer = tracker.Allow(func() {
|
||||||
|
@ -168,11 +168,12 @@ func (v *Instruments) List() List {
|
|||||||
return List{v}
|
return List{v}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Instruments) Item(i int) (name string, maxLevel float32, ok bool) {
|
func (v *Instruments) Item(i int) (name string, maxLevel float32, mute bool, ok bool) {
|
||||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||||
return "", 0, false
|
return "", 0, false, false
|
||||||
}
|
}
|
||||||
name = v.d.Song.Patch[i].Name
|
name = v.d.Song.Patch[i].Name
|
||||||
|
mute = v.d.Song.Patch[i].Mute
|
||||||
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
|
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
|
||||||
end := start + v.d.Song.Patch[i].NumVoices
|
end := start + v.d.Song.Patch[i].NumVoices
|
||||||
if end >= vm.MAX_VOICES {
|
if end >= vm.MAX_VOICES {
|
||||||
|
@ -331,6 +331,15 @@ func (p *Player) compileOrUpdateSynth() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
voice := 0
|
||||||
|
for _, instr := range p.song.Patch {
|
||||||
|
if instr.Mute {
|
||||||
|
for j := 0; j < instr.NumVoices; j++ {
|
||||||
|
p.synth.Release(voice + j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
voice += instr.NumVoices
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// all sends from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
// all sends from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
||||||
@ -387,11 +396,13 @@ func (p *Player) trigger(voiceStart, voiceEnd int, note byte, ID int) {
|
|||||||
age = p.voices[i].samplesSinceEvent
|
age = p.voices[i].samplesSinceEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
instrIndex, err := p.song.Patch.InstrumentForVoice(oldestVoice)
|
||||||
|
if err != nil || p.song.Patch[instrIndex].Mute {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.voices[oldestVoice] = voice{noteID: ID, sustain: true, samplesSinceEvent: 0}
|
p.voices[oldestVoice] = voice{noteID: ID, sustain: true, samplesSinceEvent: 0}
|
||||||
p.voiceLevels[oldestVoice] = 1.0
|
p.voiceLevels[oldestVoice] = 1.0
|
||||||
if p.synth != nil {
|
p.synth.Trigger(oldestVoice, note)
|
||||||
p.synth.Trigger(oldestVoice, note)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) release(ID int) {
|
func (p *Player) release(ID int) {
|
||||||
|
Loading…
Reference in New Issue
Block a user