mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2667c3c72c | |||
| e09af5ab34 | |||
| db2d9cac9d | |||
| a14e21dff6 | |||
| 58916d3c6d | |||
| 84d90cf0f3 | |||
| 10d20cd26f | |||
| 4a8d4c5a29 | |||
| f074c392f6 | |||
| 20fc12c529 | |||
| 6d4529971c | |||
| beb84d7652 | |||
| c55b27b23b | |||
| e488cd391b | |||
| 7f20bd8baf | |||
| 07bf8f6cdf | |||
| f0f391356c | |||
| b18a284252 | |||
| 1c020fffa3 | |||
| 267973e061 | |||
| 6b3aaf6cc9 | |||
| dfc72cd2c4 | |||
| 8a9cbdea62 | |||
| edee3452f4 | |||
| b70db4d394 | |||
| d5af39e324 | |||
| aa1b4d371b | |||
| dc12f58082 | |||
| aa7a2e56fa | |||
| 17312bbe4e | |||
| 2b3f6d8200 | |||
| db6c9f6052 | |||
| 954b306cc8 | |||
| aec756f921 | |||
| ca4a98eb50 | |||
| 65cfcb045c | |||
| bb32403c78 | |||
| d92426a100 | |||
| 6d3c65e11d | |||
| c08a319eb7 | |||
| 8227691523 | |||
| 04fbc9f6a7 |
35
.github/workflows/binaries.yml
vendored
35
.github/workflows/binaries.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
@ -74,6 +74,16 @@ jobs:
|
||||
output: sointu-track-native
|
||||
params: -tags=native cmd/sointu-track/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-vsti.so
|
||||
params: -buildmode=c-shared -tags=plugin ./cmd/sointu-vsti/
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-vsti-native.so
|
||||
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-track
|
||||
@ -82,19 +92,21 @@ jobs:
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-compile
|
||||
params: cmd/sointu-compile/main.go
|
||||
- os: macos-latest
|
||||
- os: macos-12 # this is intel still
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-track-native
|
||||
params: -tags=native cmd/sointu-track/main.go
|
||||
steps:
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
- uses: benjlevesque/short-sha@v3.0
|
||||
id: short-sha
|
||||
with:
|
||||
length: 7
|
||||
- uses: lukka/get-cmake@v3.18.3
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ilammy/setup-nasm@v1.4.0
|
||||
- uses: lukka/get-cmake@latest
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
|
||||
with:
|
||||
go-version: '>=1.21.0'
|
||||
- uses: ilammy/setup-nasm@v1.5.1
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: ${{ matrix.config.packages }}
|
||||
@ -112,9 +124,9 @@ jobs:
|
||||
run: |
|
||||
go build -o ${{ matrix.config.output }} ${{ matrix.config.params }}
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sointu-${{ runner.os }}-${{ steps.short-sha.outputs.sha }}
|
||||
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.output }}
|
||||
path: ${{ matrix.config.output }}
|
||||
upload_release_asset:
|
||||
needs: [create_release, binaries]
|
||||
@ -132,9 +144,10 @@ jobs:
|
||||
with:
|
||||
length: 7
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sointu-${{ matrix.config.os }}-${{ steps.short-sha.outputs.sha }}
|
||||
pattern: ${{ matrix.config.os }}-${{ steps.short-sha.outputs.sha }}-*
|
||||
merge-multiple: true
|
||||
path: sointu-${{ matrix.config.os }}
|
||||
- name: Zip binaries
|
||||
run: |
|
||||
|
||||
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
gotests: yes
|
||||
cgo_ldflags:
|
||||
- os: macos-latest
|
||||
- os: macos-12 # this is intel still
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
gotests: yes
|
||||
cgo_ldflags: # -Wl,-no_pie
|
||||
@ -34,16 +34,18 @@ jobs:
|
||||
# than let the tests fail because of this.
|
||||
# TODO: win32 builds didn't quite work out, complains gcc broken
|
||||
steps:
|
||||
- uses: lukka/get-cmake@v3.18.3
|
||||
- uses: vsariola/setup-wabt@v1.0.1
|
||||
- uses: lukka/get-cmake@latest
|
||||
- uses: vsariola/setup-wabt@v1.0.2
|
||||
with:
|
||||
version: 1.0.29
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
|
||||
with:
|
||||
go-version: '>=1.21.0'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '15'
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ilammy/setup-nasm@v1.4.0
|
||||
- uses: ilammy/setup-nasm@v1.5.1
|
||||
- name: Run ctest
|
||||
env:
|
||||
ASM_NASM: ${{ matrix.config.asmnasm }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -31,3 +31,6 @@ actual_output/
|
||||
**/__debug_bin
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
**/testdata/fuzz/
|
||||
.DS_Store
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@ -3,6 +3,53 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## v0.4.0
|
||||
### Added
|
||||
- User can drop preset instruments into `os.UserConfigDir()/sointu/presets/` and
|
||||
they appear in the list of presets next time sointu is started.
|
||||
([#125][i125])
|
||||
- Ability to loop certain section of the song when playing. The loop can be set
|
||||
by using the toggle button in the song panel, or by hitting Ctrl+L.
|
||||
([#128][i128])
|
||||
- Disable units temporarily. The disabled units are shown in gray and are not
|
||||
compiled into the patch and are considered for all purposes non-existent.
|
||||
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
|
||||
field `disabled: true` for the unit. ([#116][i116])
|
||||
- Passing a file name on command line immediately tries loading that file ([#122][i122])
|
||||
- Massive rewrite of the GUI, in particular allowing better copying, pasting and
|
||||
scrolling of table-based data (order list and note data).
|
||||
- Dbgain unit, which allows defining the gain in decibels (-40 dB to +40dB)
|
||||
- `+` and `-` keys add/subtract values in order editor and pattern editor
|
||||
([#65][i65])
|
||||
- The function `su_power` is exported so people can reuse it in the main code;
|
||||
however, as it assumes the parameter passed in st0 on the x87 stack and
|
||||
similarly returns it value in st0 on the x87 stack, to my knowledge there is
|
||||
no calling convention that would correspond this behaviour, so you need to
|
||||
define a header for it yourself and take care of putting the float value on
|
||||
x87 stack.
|
||||
|
||||
### Fixed
|
||||
- Loading a preset did not update the IDs of the newly loaded instrument,
|
||||
causing ID collisions and sends target wrong units.
|
||||
- The x87 native filter unit was denormalizing and eating up a lot of CPU ([#68][i68])
|
||||
- Modulating delaytime in wasm could crash, because delay time was converted to
|
||||
int with i32.trunc_f32_u. Using i32.trunc_f32_s fixed this.
|
||||
- When recording notes from VSTI, no track was created for instruments that had
|
||||
no notes triggered, resulting in misalignment of the tracks from instruments.
|
||||
- 32-bit su_load_gmdls clobbered ebx, even though __stdcall demands it to be not
|
||||
touched ([#130][i130])
|
||||
- Spaces are allowed in instrument names ([#120][i120])
|
||||
- Fixed the dropdown for targeting sends making it impossible to choose certain
|
||||
ops. This was done just by reducing the default height of popup menus so they
|
||||
fit on screen ([#121][i121])
|
||||
- Warn user about sample rate being other than 44100 Hz, as this lead to weird
|
||||
behaviour. Sointu assumes the samplerate always to be 44100 Hz. ([#129][i129])
|
||||
|
||||
### Changed
|
||||
- The scroll wheel behavior for unit integer parameters was flipped: scrolling
|
||||
up now increases the value, while scrolling down decreases the value. It was
|
||||
vice versa. ([#112][i112])
|
||||
|
||||
## v0.3.0
|
||||
### Added
|
||||
- Scroll bars to menus, shown when a menu is too long to fit.
|
||||
@ -104,7 +151,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- a command line utility to convert .yml songs to .asm
|
||||
- a command line utility to play the songs on command line
|
||||
|
||||
[Unreleased]: https://github.com/vsariola/sointu/compare/v0.3.0...HEAD
|
||||
[Unreleased]: https://github.com/vsariola/sointu/compare/v0.4.0...HEAD
|
||||
[0.4.0]: https://github.com/vsariola/sointu/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/vsariola/sointu/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/vsariola/sointu/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
|
||||
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
|
||||
[i65]: https://github.com/vsariola/sointu/issues/65
|
||||
[i68]: https://github.com/vsariola/sointu/issues/68
|
||||
[i112]: https://github.com/vsariola/sointu/issues/112
|
||||
[i116]: https://github.com/vsariola/sointu/issues/116
|
||||
[i120]: https://github.com/vsariola/sointu/issues/120
|
||||
[i121]: https://github.com/vsariola/sointu/issues/121
|
||||
[i122]: https://github.com/vsariola/sointu/issues/122
|
||||
[i125]: https://github.com/vsariola/sointu/issues/125
|
||||
[i128]: https://github.com/vsariola/sointu/issues/128
|
||||
[i129]: https://github.com/vsariola/sointu/issues/129
|
||||
[i130]: https://github.com/vsariola/sointu/issues/130
|
||||
|
||||
@ -26,6 +26,8 @@ endif()
|
||||
|
||||
IF(APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie")
|
||||
# https://stackoverflow.com/questions/69803659/what-is-the-proper-way-to-build-for-macos-x86-64-using-cmake-on-apple-m1-arm
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
|
||||
endif()
|
||||
|
||||
find_program(GO NAMES go)
|
||||
|
||||
109
README.md
109
README.md
@ -12,11 +12,13 @@ User manual will be in the [Wiki](https://github.com/vsariola/sointu/wiki).
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can either 1) download the prebuilt release binaries from the [releases](https://github.com/vsariola/sointu/releases);
|
||||
or 2) download the latest build from the master branch from the [actions](https://github.com/vsariola/sointu/actions)
|
||||
(find workflow "Binaries" and scroll down for .zip files containing the
|
||||
artifacts). Then just run one of the executables or, in the case of the
|
||||
VST plugins library files, copy them wherever you keep you VST2 plugins.
|
||||
You can either 1) download the prebuilt release binaries from the
|
||||
[releases](https://github.com/vsariola/sointu/releases); or 2) download the
|
||||
latest build from the master branch from the
|
||||
[actions](https://github.com/vsariola/sointu/actions) (find workflow "Binaries"
|
||||
and scroll down for .zip files containing the artifacts). Then just run one of
|
||||
the executables or, in the case of the VST plugins library files, copy them
|
||||
wherever you keep you VST2 plugins.
|
||||
|
||||
The pre 1.0 version tags are mostly for reference: no backwards
|
||||
compatibility will be guaranteed while upgrading to a newer version.
|
||||
@ -48,9 +50,9 @@ Sointu consists of two core elements:
|
||||
.yml files.
|
||||
- A compiler, likewise written in go, which can be invoked from the command line
|
||||
to compile these .yml files into .asm or .wat code. For x86/amd64, the
|
||||
resulting .asm can be then compiled by [nasm](https://www.nasm.us/) or
|
||||
[yasm](https://yasm.tortall.net). For browsers, the resulting .wat can be
|
||||
compiled by [wat2wasm](https://github.com/WebAssembly/wabt).
|
||||
resulting .asm can be then compiled by [nasm](https://www.nasm.us/). For
|
||||
browsers, the resulting .wat can be compiled by
|
||||
[wat2wasm](https://github.com/WebAssembly/wabt).
|
||||
|
||||
This is how the current prototype app looks like:
|
||||
|
||||
@ -131,15 +133,16 @@ a dynamically linked library and ran inside a VST host.
|
||||
go build -buildmode=c-shared -tags=plugin -o sointu-vsti.dll .\cmd\sointu-vsti\
|
||||
```
|
||||
|
||||
On other platforms than Windows, replace `-o sointu-track.dll`
|
||||
appropriately e.g. `-o sointu-track.so`; however, the VST instrument is
|
||||
completely untested on all other platforms than Windows at the moment.
|
||||
On other platforms than Windows, replace `-o sointu-vsti.dll` appropriately e.g.
|
||||
`-o sointu-vsti.so`; so far, the VST instrument has been built & tested on
|
||||
Windows and Linux.
|
||||
|
||||
Notice the `-tags=plugin` build tag definition. This is required by the [vst2 library](https://github.com/pipelined/vst2);
|
||||
otherwise, you will get a lot of build errors.
|
||||
Notice the `-tags=plugin` build tag definition. This is required by the [vst2
|
||||
library](https://github.com/pipelined/vst2); otherwise, you will get a lot of
|
||||
build errors.
|
||||
|
||||
Add `-tags=native,plugin` to use the [x86 native virtual machine](#native-virtual-machine)
|
||||
instead of the virtual machine written in Go.
|
||||
Add `-tags=native,plugin` to use the [x86 native virtual
|
||||
machine](#native-virtual-machine) instead of the virtual machine written in Go.
|
||||
|
||||
### Sointu-compile
|
||||
|
||||
@ -159,7 +162,7 @@ go run cmd/sointu-compile/main.go
|
||||
go build -o sointu-compile.exe cmd/sointu-compile/main.go
|
||||
```
|
||||
|
||||
On other platforms than Windows, replace `-o sointu-compile-exe` with
|
||||
On other platforms than Windows, replace `-o sointu-compile.exe` with
|
||||
`-o sointu-compile`.
|
||||
|
||||
#### Usage
|
||||
@ -179,32 +182,37 @@ sointu-compile -o . -arch=wasm tests/test_chords.yml
|
||||
wat2wasm --enable-bulk-memory test_chords.wat
|
||||
```
|
||||
|
||||
If you are looking for an easy way to compile an executable from a Sointu song
|
||||
(e.g. for a executable music compo), take a look at [NR4's Python-based
|
||||
tool](https://github.com/LeStahL/sointu-executable-msx) for it.
|
||||
|
||||
#### Examples
|
||||
|
||||
The folder `examples/code` contains usage examples for Sointu with winmm
|
||||
and dsound playback under Windows and asound playback under Unix. Source
|
||||
code is available in C and x86 assembly (win32, elf32 and elf64
|
||||
versions).
|
||||
The folder `examples/code` contains usage examples for Sointu with winmm and
|
||||
dsound playback under Windows and asound playback under Unix. Source code is
|
||||
available in C and x86 assembly (win32, elf32 and elf64 versions).
|
||||
|
||||
To build the examples, use `ninja examples`.
|
||||
|
||||
If you want to target smaller executable sizes, using a compressing linker
|
||||
like [Crinkler](https://github.com/runestubbe/Crinkler) on Windows is recommended.
|
||||
If you want to target smaller executable sizes, using a compressing linker like
|
||||
[Crinkler](https://github.com/runestubbe/Crinkler) on Windows is recommended.
|
||||
|
||||
The linux examples use ALSA and need libasound2-dev (or libasound2-dev:386) installed. The 386 version also needs pipewire-alsa:386 installed, which is not there by default.
|
||||
The linux examples use ALSA and need libasound2-dev (or libasound2-dev:386)
|
||||
installed. The 386 version also needs pipewire-alsa:386 installed, which is not
|
||||
there by default.
|
||||
|
||||
### Native virtual machine
|
||||
|
||||
The native bridge allows Go to call the sointu compiled x86 native
|
||||
virtual machine, through cgo, instead of using the Go written bytecode
|
||||
interpreter. It's likely slightly faster than the interpreter. Before
|
||||
you can actually run it, you need to build the bridge using CMake (thus,
|
||||
***this will not work with go get***).
|
||||
The native bridge allows Go to call the sointu compiled x86 native virtual
|
||||
machine, through cgo, instead of using the Go written bytecode interpreter. It's
|
||||
likely slightly faster than the interpreter. Before you can actually run it, you
|
||||
need to build the bridge using CMake (thus, ***this will not work with go
|
||||
get***).
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [CMake](https://cmake.org)
|
||||
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
|
||||
- [nasm](https://www.nasm.us/)
|
||||
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
|
||||
best bet is [MinGW](http://www.mingw.org/). We use the
|
||||
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
|
||||
@ -252,15 +260,16 @@ go run -tags=native cmd/sointu-play/main.go tests/test_chords.yml
|
||||
> opcodes). In future, the app should give warnings if the user is about to
|
||||
> exceed the capabilities of a target platform.
|
||||
|
||||
> :warning: **If you are using MinGW and Yasm**: Yasm 1.3.0 (currently still the
|
||||
> latest stable release) and GNU linker do not play nicely along, trashing the
|
||||
> BSS layout. See
|
||||
> :warning: **If you are using Yasm instead of Nasm, and you are using MinGW**:
|
||||
> Yasm 1.3.0 (currently still the latest stable release) and GNU linker do not
|
||||
> play nicely along, trashing the BSS layout. The linker had placed our synth
|
||||
> object overlapping with DLL call addresses; very funny stuff to debug. See
|
||||
> [here](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
|
||||
> and the fix
|
||||
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
|
||||
> Use a newer nightly build of yasm that includes the fix. The linker had placed
|
||||
> our synth object overlapping with DLL call addresses; very funny stuff to
|
||||
> debug.
|
||||
> Since Nasm is nowadays under BSD license, there is absolutely no reason to use
|
||||
> Yasm. However, if you do, use a newer nightly build of Yasm that includes the
|
||||
> fix.
|
||||
|
||||
### Tests
|
||||
|
||||
@ -272,7 +281,7 @@ intro.
|
||||
|
||||
- [go](https://golang.org/)
|
||||
- [CMake](https://cmake.org) with CTest
|
||||
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
|
||||
- [nasm](https://www.nasm.us/)
|
||||
- Your favorite CMake compatible c-compiler & build tool. Results have been
|
||||
obtained using Visual Studio 2019, gcc&make on linux, MinGW&mingw32-make, and
|
||||
ninja&AppleClang.
|
||||
@ -316,8 +325,8 @@ New features since fork
|
||||
entropy as low as possible, yet we can call arbitrary go functions as
|
||||
"macros". The templates are [here](templates/) and the compiler lives
|
||||
[here](vm/compiler/).
|
||||
- **Tracker**. Written in go. A crude version exists. Can run either
|
||||
as a stand-alone app or a vsti plugin.
|
||||
- **Tracker**. Written in go. Can run either as a stand-alone app or a vsti
|
||||
plugin.
|
||||
- **Supports 32 and 64 bit builds**. The 64-bit version is done with minimal
|
||||
changes to get it work, using template macros to change the lines between
|
||||
32-bit and 64-bit modes. Mostly, it's as easy as writing {{.AX}} instead of
|
||||
@ -406,20 +415,6 @@ New features since fork
|
||||
- **A bytecode interpreter written in pure go**. It's slightly slower than the
|
||||
hand-written assembly code by sointu compiler, but with this, the tracker is
|
||||
ultraportable and does not need cgo calls.
|
||||
- **Using Sointu as a sync-tracker**. Similar to [GNU
|
||||
Rocket](https://github.com/rocket/rocket), but (ab)using the tracker we
|
||||
already have for music. We use the Go rpc package to send current sync
|
||||
values from the new SYNC opcode + optionally the current fractional row the
|
||||
song is on. The syncs are saved every 256th sample (approximately 172 Hz).
|
||||
For 4k intro development, the idea is to write a debug version of the intro
|
||||
that merely loads the shader and listens to the RPC messages, and then draws
|
||||
the shader with those as the uniforms. Then, during the actual 4k intro, one
|
||||
can get the sync data from Sointu: if the song uses syncs, su_render_song
|
||||
writes the syncs to a float array. During each time step, a slice of this
|
||||
array can be sent to the shader as a uniform float array. A track with two
|
||||
voices, triggering an instrument with a single envelope and a slow filter,
|
||||
can even be used as a cheap smooth interpolation mechanism, provided the
|
||||
syncs are added to each other in the shader.
|
||||
|
||||
Future goals
|
||||
------------
|
||||
@ -511,8 +506,10 @@ Prods using Sointu
|
||||
- [Delusions of mediocrity](https://www.pouet.net/prod.php?which=95222) by
|
||||
mrange & Virgill
|
||||
- [Xorverse](https://www.pouet.net/prod.php?which=95221) by Alcatraz
|
||||
- [l'enveloppe](https://www.pouet.net/prod.php?which=95215) by Team210 &
|
||||
epoqe
|
||||
- [l'enveloppe](https://www.pouet.net/prod.php?which=95215) by Team210 & epoqe
|
||||
- [Phosphorescent Purple Pixel Peaks](https://www.pouet.net/prod.php?which=96198) by mrange & Virgill
|
||||
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
|
||||
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
|
||||
|
||||
Contributing
|
||||
------------
|
||||
@ -528,7 +525,7 @@ Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
|
||||
Contact
|
||||
-------
|
||||
|
||||
Veikko Sariola - pestis_bc on discord - firstname.lastname@gmail.com
|
||||
Veikko Sariola - pestis_bc on Demoscene discord - firstname.lastname@gmail.com
|
||||
|
||||
Project Link: [https://github.com/vsariola/sointu](https://github.com/vsariola/sointu)
|
||||
|
||||
@ -540,4 +537,4 @@ The original 4klang: Dominik Ries ([gopher/Alcatraz](https://github.com/hzdgophe
|
||||
|
||||
Sointu: Veikko Sariola (pestis/bC!), [Apollo/bC!](https://github.com/moitias),
|
||||
[NR4/Team210](https://github.com/LeStahL/), [PoroCYon](https://github.com/PoroCYon/4klang),
|
||||
[kendfss](https://github.com/kendfss)
|
||||
[kendfss](https://github.com/kendfss), [anticore](https://github.com/anticore)
|
||||
|
||||
5
audio.go
5
audio.go
@ -66,7 +66,7 @@ type (
|
||||
|
||||
// Play plays the Song by first compiling the patch with the given Synther,
|
||||
// returning the stereo audio buffer as a result (and possible errors).
|
||||
func Play(synther Synther, song Song) (AudioBuffer, error) {
|
||||
func Play(synther Synther, song Song, progress func(float32)) (AudioBuffer, error) {
|
||||
err := song.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -125,6 +125,9 @@ func Play(synther Synther, song Song) (AudioBuffer, error) {
|
||||
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
|
||||
}
|
||||
}
|
||||
if progress != nil {
|
||||
progress(float32(row+1) / float32(song.Score.LengthInRows()))
|
||||
}
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ func main() {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song) // render the song to calculate its length
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil) // render the song to calculate its length
|
||||
if err != nil {
|
||||
return fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
|
||||
@ -33,16 +33,16 @@ var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
var f *os.File
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
var err error
|
||||
f, err = os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
defer f.Close() // error handling omitted for example
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
audioContext, err := oto.NewContext()
|
||||
if err != nil {
|
||||
@ -50,15 +50,19 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer audioContext.Close()
|
||||
modelMessages := make(chan interface{}, 1024)
|
||||
playerMessages := make(chan tracker.PlayerMessage, 1024)
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
|
||||
}
|
||||
model := tracker.NewModel(modelMessages, playerMessages, recoveryFile)
|
||||
player := tracker.NewPlayer(cmd.MainSynther, playerMessages, modelMessages)
|
||||
tracker := gioui.NewTracker(model, cmd.MainSynther)
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
|
||||
if a := flag.Args(); len(a) > 0 {
|
||||
f, err := os.Open(a[0])
|
||||
if err == nil {
|
||||
model.ReadSong(f)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
tracker := gioui.NewTracker(model)
|
||||
output := audioContext.Output()
|
||||
defer output.Close()
|
||||
go func() {
|
||||
@ -71,6 +75,10 @@ func main() {
|
||||
}()
|
||||
go func() {
|
||||
tracker.Main()
|
||||
if *cpuprofile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
if err != nil {
|
||||
|
||||
@ -5,8 +5,11 @@ package main
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
@ -54,19 +57,23 @@ func init() {
|
||||
version = int32(100)
|
||||
)
|
||||
vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) {
|
||||
modelMessages := make(chan interface{}, 1024)
|
||||
playerMessages := make(chan tracker.PlayerMessage, 1024)
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
|
||||
recoveryFile = filepath.Join(configDir, "sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
|
||||
}
|
||||
model := tracker.NewModel(modelMessages, playerMessages, recoveryFile)
|
||||
player := tracker.NewPlayer(cmd.MainSynther, playerMessages, modelMessages)
|
||||
tracker := gioui.NewTracker(model, cmd.MainSynther)
|
||||
tracker.SetInstrEnlarged(true) // start the vsti with the instrument editor enlarged
|
||||
go tracker.Main()
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
|
||||
t := gioui.NewTracker(model)
|
||||
tracker.Bool{BoolData: (*tracker.InstrEnlarged)(model)}.Set(true)
|
||||
if s := h.GetSampleRate(); math.Abs(float64(h.GetSampleRate()-44100.0)) > 1e-6 {
|
||||
model.Alerts().AddAlert(tracker.Alert{
|
||||
Message: fmt.Sprintf("VSTi host sample rate is %.0f Hz; sointu supports 44100 Hz only", s),
|
||||
Priority: tracker.Error,
|
||||
Duration: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
go t.Main()
|
||||
context := VSTIProcessContext{host: h}
|
||||
buf := make(sointu.AudioBuffer, 1024)
|
||||
return vst2.Plugin{
|
||||
@ -110,14 +117,16 @@ func init() {
|
||||
}
|
||||
},
|
||||
CloseFunc: func() {
|
||||
tracker.Quit(true)
|
||||
tracker.WaitQuitted()
|
||||
t.Exec() <- func() { t.ForceQuit().Do() }
|
||||
t.WaitQuitted()
|
||||
},
|
||||
GetChunkFunc: func(isPreset bool) []byte {
|
||||
return tracker.SafeMarshalRecovery()
|
||||
retChn := make(chan []byte)
|
||||
t.Exec() <- func() { retChn <- t.MarshalRecovery() }
|
||||
return <-retChn
|
||||
},
|
||||
SetChunkFunc: func(data []byte, isPreset bool) {
|
||||
tracker.SafeUnmarshalRecovery(data)
|
||||
t.Exec() <- func() { t.UnmarshalRecovery(data) }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
14
examples/code/wasm/README.md
Normal file
14
examples/code/wasm/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
Requirements: sointu binaries, `wabt`
|
||||
|
||||
To generate the .wasm file:
|
||||
|
||||
```
|
||||
sointu-compile -o . -arch=wasm tests/test_chords.yml
|
||||
wat2wasm --enable-bulk-memory test_chords.wat
|
||||
```
|
||||
|
||||
To run the example:
|
||||
|
||||
```
|
||||
npx serve examples/code/wasm
|
||||
```
|
||||
51
examples/code/wasm/index.html
Normal file
51
examples/code/wasm/index.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>sointu WASM example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="module">
|
||||
// button to start audio context
|
||||
const button = document.createElement("button");
|
||||
button.innerHTML = "start";
|
||||
document.body.appendChild(button);
|
||||
button.onclick = () => {
|
||||
document.body.removeChild(button);
|
||||
|
||||
fetch("test_chords.wasm")
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((bytes) => WebAssembly.instantiate(bytes, { m: Math }))
|
||||
.then(({ instance }) => {
|
||||
const context = new AudioContext({ sampleRate: 44100 });
|
||||
|
||||
let frames = instance.exports.t.value
|
||||
? instance.exports.l.value / 4
|
||||
: instance.exports.l.value / 8;
|
||||
|
||||
let wasmBuffer = new Float32Array(
|
||||
instance.exports.m.buffer,
|
||||
instance.exports.s.value,
|
||||
frames * 2
|
||||
);
|
||||
|
||||
const buffer = context.createBuffer(2, frames, context.sampleRate);
|
||||
|
||||
// convert wasm buffer to audio buffer
|
||||
for (let channel = 0; channel < 2; channel++) {
|
||||
const buffering = buffer.getChannelData(channel);
|
||||
for (let i = 0; i < frames; i++) {
|
||||
buffering[i] = wasmBuffer[i * 2 + channel];
|
||||
}
|
||||
}
|
||||
|
||||
// connect to output and start playing
|
||||
const src = context.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
src.connect(context.destination);
|
||||
src.start();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
examples/code/wasm/test_chords.wasm
Normal file
BIN
examples/code/wasm/test_chords.wasm
Normal file
Binary file not shown.
14
go.mod
14
go.mod
@ -1,17 +1,17 @@
|
||||
module github.com/vsariola/sointu
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
gioui.org v0.3.1
|
||||
gioui.org/x v0.1.0
|
||||
gioui.org v0.5.0
|
||||
gioui.org/x v0.5.0
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||
github.com/hajimehoshi/oto v0.6.6
|
||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
||||
golang.org/x/text v0.9.0
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||
pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826
|
||||
pipelined.dev/audio/vst2 v0.10.1-0.20240223162706-41e9b65fb5c2
|
||||
)
|
||||
|
||||
require (
|
||||
@ -29,10 +29,10 @@ require (
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
pipelined.dev/pipe v0.11.0 // indirect
|
||||
pipelined.dev/signal v0.10.0 // indirect
|
||||
)
|
||||
|
||||
22
go.sum
22
go.sum
@ -1,13 +1,14 @@
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
|
||||
gioui.org v0.3.1 h1:hslYkrkIWvx28Mxe3A87opl+8s9mnWsnWmPDh11+zco=
|
||||
gioui.org v0.3.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q=
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
|
||||
gioui.org v0.5.0 h1:07g7/LY1MFuTncfO4A5DIKMMsQV6PkPHyx0MhDqgmYY=
|
||||
gioui.org v0.5.0/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q=
|
||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
gioui.org/x v0.1.0 h1:CvphvaQSroRaNEZ+JbXBkV3J3klA76U3JpieyEwHFX4=
|
||||
gioui.org/x v0.1.0/go.mod h1:5qZxjtK/TVznMlcEOyn8OheiCZlArxF3IKnLqSehKXQ=
|
||||
gioui.org/x v0.5.0 h1:NVKTn5AZuYhkAnF7MYcy1dIes36+U1N4gUTsgBhfr4A=
|
||||
gioui.org/x v0.5.0/go.mod h1:X4UBhvanAN+8S16L3K6jDMrVo7Dii7NptgBpOLBD7E4=
|
||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
|
||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
@ -22,6 +23,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
@ -49,8 +51,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5U
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
|
||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
|
||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
@ -84,8 +86,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@ -110,8 +112,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826 h1:4c7O6PJ/Zl677O2VhXHUZK7LJyVBhUI7Q39+ri+gKUs=
|
||||
pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826/go.mod h1:wETLxsbBPftj6t4iVBCXvH/Xgd27ZgIC4hNnHDYNuz8=
|
||||
pipelined.dev/audio/vst2 v0.10.1-0.20240223162706-41e9b65fb5c2 h1:qrI7YY5ZH4pJflMfzum2TKvA1NaX+H4feaA6jweX2R8=
|
||||
pipelined.dev/audio/vst2 v0.10.1-0.20240223162706-41e9b65fb5c2/go.mod h1:wETLxsbBPftj6t4iVBCXvH/Xgd27ZgIC4hNnHDYNuz8=
|
||||
pipelined.dev/pipe v0.10.0/go.mod h1:aIt+NPlW0QLYByqYniG77lTxSvl7OtCNLws/m+Xz5ww=
|
||||
pipelined.dev/pipe v0.11.0 h1:yRrbntKdqw/nbFqkz9dPaSHBoM7pK1LRHHDqdBJuqtc=
|
||||
pipelined.dev/pipe v0.11.0/go.mod h1:aIt+NPlW0QLYByqYniG77lTxSvl7OtCNLws/m+Xz5ww=
|
||||
|
||||
24
patch.go
24
patch.go
@ -44,6 +44,10 @@ type (
|
||||
// unit, VarArgs is the delaytimes, in samples, of the different delaylines
|
||||
// in the unit.
|
||||
VarArgs []int `yaml:",flow,omitempty"`
|
||||
|
||||
// Disabled is a flag that can be set to true to disable the unit.
|
||||
// Disabled units are considered to be not present in the patch.
|
||||
Disabled bool `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// UnitParameter documents one parameter that an unit takes
|
||||
@ -82,6 +86,9 @@ var UnitTypes = map[string]([]UnitParameter){
|
||||
"invgain": []UnitParameter{
|
||||
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
||||
"dbgain": []UnitParameter{
|
||||
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||
{Name: "decibels", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
||||
"filter": []UnitParameter{
|
||||
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
||||
@ -217,7 +224,7 @@ func (u *Unit) Copy() Unit {
|
||||
}
|
||||
varArgs := make([]int, len(u.VarArgs))
|
||||
copy(varArgs, u.VarArgs)
|
||||
return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID}
|
||||
return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID, Disabled: u.Disabled}
|
||||
}
|
||||
|
||||
// StackChange returns how this unit will affect the signal stack. "pop" and
|
||||
@ -227,6 +234,9 @@ func (u *Unit) Copy() Unit {
|
||||
// unit). Effects that just change the topmost signal and will not change the
|
||||
// number of signals on the stack and thus return 0.
|
||||
func (u *Unit) StackChange() int {
|
||||
if u.Disabled {
|
||||
return 0
|
||||
}
|
||||
switch u.Type {
|
||||
case "addp", "mulp", "pop", "out", "outaux", "aux":
|
||||
return -1 - u.Parameters["stereo"]
|
||||
@ -246,6 +256,9 @@ func (u *Unit) StackChange() int {
|
||||
// this unit is executed. Used to prevent stack underflow. Units producing
|
||||
// signals do not care what is on the stack before and will return 0.
|
||||
func (u *Unit) StackNeed() int {
|
||||
if u.Disabled {
|
||||
return 0
|
||||
}
|
||||
switch u.Type {
|
||||
case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
||||
return 0
|
||||
@ -347,14 +360,14 @@ func (p Patch) InstrumentForVoice(voice int) (int, error) {
|
||||
// given id. Two units should never have the same id, but if they do, then the
|
||||
// first match is returned. Id 0 is interpreted as "no id", thus searching for
|
||||
// id 0 returns an error. Error is also returned if the searched id is not
|
||||
// found.
|
||||
// found. FindUnit considers disabled units as non-existent.
|
||||
func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) {
|
||||
if id == 0 {
|
||||
return 0, 0, errors.New("FindUnit called with id 0")
|
||||
}
|
||||
for i, instr := range p {
|
||||
for u, unit := range instr.Units {
|
||||
if unit.ID == id {
|
||||
if unit.ID == id && !unit.Disabled {
|
||||
return i, u, nil
|
||||
}
|
||||
}
|
||||
@ -494,6 +507,11 @@ func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
|
||||
return "aux3 right"
|
||||
}
|
||||
}
|
||||
case "dbgain":
|
||||
switch param {
|
||||
case "decibels":
|
||||
return fmt.Sprintf("%.2f dB", 40*(float32(value)/64-1))
|
||||
}
|
||||
case "crush":
|
||||
switch param {
|
||||
case "resolution":
|
||||
|
||||
92
song.go
92
song.go
@ -67,8 +67,46 @@ type (
|
||||
// the slice only by necessary amount when a new item is added, filling the
|
||||
// unused slots with -1s.
|
||||
Order []int
|
||||
|
||||
// SongPos represents a position in a song, in terms of order row and
|
||||
// pattern row. The order row is the index of the pattern in the order list,
|
||||
// and the pattern row is the index of the row in the pattern.
|
||||
SongPos struct {
|
||||
OrderRow int
|
||||
PatternRow int
|
||||
}
|
||||
)
|
||||
|
||||
func (s *Score) SongPos(songRow int) SongPos {
|
||||
if s.RowsPerPattern == 0 {
|
||||
return SongPos{OrderRow: 0, PatternRow: 0}
|
||||
}
|
||||
orderRow := songRow / s.RowsPerPattern
|
||||
patternRow := songRow % s.RowsPerPattern
|
||||
return SongPos{OrderRow: orderRow, PatternRow: patternRow}
|
||||
}
|
||||
|
||||
func (s *Score) SongRow(songPos SongPos) int {
|
||||
return songPos.OrderRow*s.RowsPerPattern + songPos.PatternRow
|
||||
}
|
||||
|
||||
func (s *Score) Wrap(songPos SongPos) SongPos {
|
||||
ret := s.SongPos(s.SongRow(songPos))
|
||||
ret.OrderRow %= s.Length
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Score) Clamp(songPos SongPos) SongPos {
|
||||
r := s.SongRow(songPos)
|
||||
if l := s.LengthInRows(); r >= l {
|
||||
r = l - 1
|
||||
}
|
||||
if r < 0 {
|
||||
r = 0
|
||||
}
|
||||
return s.SongPos(r)
|
||||
}
|
||||
|
||||
// Get returns the value at index; or -1 is the index is out of range
|
||||
func (s Order) Get(index int) int {
|
||||
if index < 0 || index >= len(s) {
|
||||
@ -85,6 +123,55 @@ func (s *Order) Set(index, value int) {
|
||||
(*s)[index] = value
|
||||
}
|
||||
|
||||
func (s Track) Note(pos SongPos) byte {
|
||||
if pos.OrderRow < 0 || pos.OrderRow >= len(s.Order) {
|
||||
return 1
|
||||
}
|
||||
pat := s.Order[pos.OrderRow]
|
||||
if pat < 0 || pat >= len(s.Patterns) {
|
||||
return 1
|
||||
}
|
||||
if pos.PatternRow < 0 || pos.PatternRow >= len(s.Patterns[pat]) {
|
||||
return 1
|
||||
}
|
||||
return s.Patterns[pat][pos.PatternRow]
|
||||
}
|
||||
|
||||
func (s *Track) SetNote(pos SongPos, note byte) {
|
||||
if pos.OrderRow < 0 || pos.PatternRow < 0 {
|
||||
return
|
||||
}
|
||||
pat := s.Order.Get(pos.OrderRow)
|
||||
if pat < 0 {
|
||||
if note == 1 {
|
||||
return
|
||||
}
|
||||
for _, o := range s.Order {
|
||||
if pat <= o {
|
||||
pat = o
|
||||
}
|
||||
}
|
||||
pat += 1
|
||||
if pat >= 36 {
|
||||
return
|
||||
}
|
||||
s.Order.Set(pos.OrderRow, pat)
|
||||
}
|
||||
if pat >= len(s.Patterns) && note == 1 {
|
||||
return
|
||||
}
|
||||
for pat >= len(s.Patterns) {
|
||||
s.Patterns = append(s.Patterns, Pattern{})
|
||||
}
|
||||
if pos.PatternRow >= len(s.Patterns[pat]) && note == 1 {
|
||||
return
|
||||
}
|
||||
for pos.PatternRow >= len(s.Patterns[pat]) {
|
||||
s.Patterns[pat] = append(s.Patterns[pat], 1)
|
||||
}
|
||||
s.Patterns[pat][pos.PatternRow] = note
|
||||
}
|
||||
|
||||
// Get returns the value at index; or 1 is the index is out of range
|
||||
func (s Pattern) Get(index int) byte {
|
||||
if index < 0 || index >= len(s) {
|
||||
@ -165,7 +252,10 @@ func (s *Song) Copy() Song {
|
||||
// Assuming 44100 Hz playback speed, return the number of samples of each row of
|
||||
// the song.
|
||||
func (s *Song) SamplesPerRow() int {
|
||||
return 44100 * 60 / (s.BPM * s.RowsPerBeat)
|
||||
if divisor := s.BPM * s.RowsPerBeat; divisor > 0 {
|
||||
return 44100 * 60 / divisor
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate checks if the Song looks like a valid song: BPM > 0, one or more
|
||||
|
||||
@ -72,6 +72,8 @@ regression_test(test_gain LOADVAL GAIN)
|
||||
regression_test(test_gain_stereo GAIN)
|
||||
regression_test(test_invgain LOADVAL INVGAIN)
|
||||
regression_test(test_invgain_stereo INVGAIN)
|
||||
regression_test(test_dbgain LOADVAL DBGAIN)
|
||||
regression_test(test_dbgain_stereo DBGAIN)
|
||||
regression_test(test_send LOADVAL SEND)
|
||||
regression_test(test_send_stereo SEND)
|
||||
regression_test(test_send_global SEND SEND_GLOBAL)
|
||||
|
||||
1
tests/expected_output/test_dbgain.raw
Normal file
1
tests/expected_output/test_dbgain.raw
Normal file
File diff suppressed because one or more lines are too long
1
tests/expected_output/test_dbgain_stereo.raw
Normal file
1
tests/expected_output/test_dbgain_stereo.raw
Normal file
File diff suppressed because one or more lines are too long
22
tests/test_dbgain.yml
Normal file
22
tests/test_dbgain.yml
Normal file
@ -0,0 +1,22 @@
|
||||
bpm: 100
|
||||
rowsperbeat: 4
|
||||
score:
|
||||
rowsperpattern: 16
|
||||
length: 1
|
||||
tracks:
|
||||
- numvoices: 1
|
||||
order: [0]
|
||||
patterns: [[64, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]]
|
||||
patch:
|
||||
- numvoices: 1
|
||||
units:
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 0}
|
||||
- type: dbgain
|
||||
parameters: {decibels: 32, stereo: 0}
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 128}
|
||||
- type: dbgain
|
||||
parameters: {decibels: 32, stereo: 0}
|
||||
- type: out
|
||||
parameters: {gain: 128, stereo: 1}
|
||||
20
tests/test_dbgain_stereo.yml
Normal file
20
tests/test_dbgain_stereo.yml
Normal file
@ -0,0 +1,20 @@
|
||||
bpm: 100
|
||||
rowsperbeat: 4
|
||||
score:
|
||||
rowsperpattern: 16
|
||||
length: 1
|
||||
tracks:
|
||||
- numvoices: 1
|
||||
order: [0]
|
||||
patterns: [[64, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]]
|
||||
patch:
|
||||
- numvoices: 1
|
||||
units:
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 0}
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 128}
|
||||
- type: dbgain
|
||||
parameters: {decibels: 32, stereo: 1}
|
||||
- type: out
|
||||
parameters: {gain: 128, stereo: 1}
|
||||
@ -11,10 +11,12 @@ patch:
|
||||
- numvoices: 1
|
||||
units:
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 0}
|
||||
parameters: {stereo: 0, value: 32}
|
||||
- type: gain
|
||||
parameters: {gain: 128, stereo: 0}
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 128}
|
||||
- type: gain
|
||||
parameters: {gain: 64, stereo: 1}
|
||||
parameters: {gain: 64, stereo: 0}
|
||||
- type: out
|
||||
parameters: {gain: 128, stereo: 1}
|
||||
|
||||
@ -11,12 +11,10 @@ patch:
|
||||
- numvoices: 1
|
||||
units:
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 32}
|
||||
- type: gain
|
||||
parameters: {gain: 128, stereo: 0}
|
||||
parameters: {stereo: 0, value: 0}
|
||||
- type: loadval
|
||||
parameters: {stereo: 0, value: 128}
|
||||
- type: gain
|
||||
parameters: {gain: 64, stereo: 0}
|
||||
parameters: {gain: 64, stereo: 1}
|
||||
- type: out
|
||||
parameters: {gain: 128, stereo: 1}
|
||||
|
||||
409
tracker/action.go
Normal file
409
tracker/action.go
Normal file
@ -0,0 +1,409 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
// Action describes a user action that can be performed on the model. It is
|
||||
// usually a button press or a menu item. Action advertises whether it is
|
||||
// allowed to be performed or not.
|
||||
Action struct {
|
||||
do func()
|
||||
allowed func() bool
|
||||
}
|
||||
)
|
||||
|
||||
// Action methods
|
||||
|
||||
func (e Action) Do() {
|
||||
if e.allowed != nil && e.allowed() {
|
||||
e.do()
|
||||
}
|
||||
}
|
||||
|
||||
func (e Action) Allowed() bool {
|
||||
return e.allowed != nil && e.allowed()
|
||||
}
|
||||
|
||||
func Allow(do func()) Action {
|
||||
return Action{do: do, allowed: func() bool { return true }}
|
||||
}
|
||||
|
||||
func Check(do func(), allowed func() bool) Action {
|
||||
return Action{do: do, allowed: allowed}
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) AddTrack() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("AddTrackAction", ScoreChange, MajorChange)()
|
||||
if len(m.d.Song.Score.Tracks) == 0 { // no instruments, add one
|
||||
m.d.Cursor.Track = 0
|
||||
} else {
|
||||
m.d.Cursor.Track++
|
||||
}
|
||||
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)), 0)
|
||||
newTracks := make([]sointu.Track, len(m.d.Song.Score.Tracks)+1)
|
||||
copy(newTracks, m.d.Song.Score.Tracks[:m.d.Cursor.Track])
|
||||
copy(newTracks[m.d.Cursor.Track+1:], m.d.Song.Score.Tracks[m.d.Cursor.Track:])
|
||||
newTracks[m.d.Cursor.Track] = sointu.Track{
|
||||
NumVoices: 1,
|
||||
Patterns: []sointu.Pattern{},
|
||||
}
|
||||
m.d.Song.Score.Tracks = newTracks
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) DeleteTrack() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return len(m.d.Song.Score.Tracks) > 0 },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteTrackAction", ScoreChange, MajorChange)()
|
||||
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
newTracks := make([]sointu.Track, len(m.d.Song.Score.Tracks)-1)
|
||||
copy(newTracks, m.d.Song.Score.Tracks[:m.d.Cursor.Track])
|
||||
copy(newTracks[m.d.Cursor.Track:], m.d.Song.Score.Tracks[m.d.Cursor.Track+1:])
|
||||
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
m.d.Song.Score.Tracks = newTracks
|
||||
m.d.Cursor2 = m.d.Cursor
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddInstrument() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("AddInstrumentAction", PatchChange, MajorChange)()
|
||||
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
||||
m.d.InstrIndex = 0
|
||||
} else {
|
||||
m.d.InstrIndex++
|
||||
}
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, sointu.Instrument{})
|
||||
copy(m.d.Song.Patch[m.d.InstrIndex+1:], m.d.Song.Patch[m.d.InstrIndex:])
|
||||
newInstr := defaultInstrument.Copy()
|
||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||
m.d.InstrIndex2 = m.d.InstrIndex
|
||||
m.d.UnitIndex = 0
|
||||
m.d.ParamIndex = 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) DeleteInstrument() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return len((*Model)(m).d.Song.Patch) > 0 },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteInstrumentAction", PatchChange, MajorChange)()
|
||||
m.d.Song.Patch = append(m.d.Song.Patch[:m.d.InstrIndex], m.d.Song.Patch[m.d.InstrIndex+1:]...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddUnit(before bool) Action {
|
||||
return Allow(func() {
|
||||
defer (*Model)(m).change("AddUnitAction", PatchChange, MajorChange)()
|
||||
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
||||
instr := sointu.Instrument{NumVoices: 1}
|
||||
instr.Units = make([]sointu.Unit, 0, 1)
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, instr)
|
||||
m.d.UnitIndex = 0
|
||||
} else {
|
||||
if !before {
|
||||
m.d.UnitIndex++
|
||||
}
|
||||
}
|
||||
m.d.InstrIndex = intMax(intMin(m.d.InstrIndex, len(m.d.Song.Patch)-1), 0)
|
||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||
newUnits := make([]sointu.Unit, len(instr.Units)+1)
|
||||
m.d.UnitIndex = clamp(m.d.UnitIndex, 0, len(newUnits)-1)
|
||||
m.d.UnitIndex2 = m.d.UnitIndex
|
||||
copy(newUnits, instr.Units[:m.d.UnitIndex])
|
||||
copy(newUnits[m.d.UnitIndex+1:], instr.Units[m.d.UnitIndex:])
|
||||
(*Model)(m).assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1])
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = newUnits
|
||||
m.d.ParamIndex = 0
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) DeleteUnit() Action {
|
||||
return Action{
|
||||
allowed: func() bool {
|
||||
return len((*Model)(m).d.Song.Patch) > 0 && len((*Model)(m).d.Song.Patch[(*Model)(m).d.InstrIndex].Units) > 1
|
||||
},
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
||||
m.Units().List().DeleteElements(true)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) ClearUnit() Action {
|
||||
return Action{
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
||||
m.d.UnitIndex = intMax(intMin(m.d.UnitIndex, len(m.d.Song.Patch[m.d.InstrIndex].Units)-1), 0)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = sointu.Unit{}
|
||||
},
|
||||
allowed: func() bool {
|
||||
return m.d.InstrIndex >= 0 &&
|
||||
m.d.InstrIndex < len(m.d.Song.Patch) &&
|
||||
len(m.d.Song.Patch[m.d.InstrIndex].Units) > 0
|
||||
},
|
||||
}
|
||||
}
|
||||
func (m *Model) Undo() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return len((*Model)(m).undoStack) > 0 },
|
||||
do: func() {
|
||||
m.redoStack = append(m.redoStack, m.d.Copy())
|
||||
if len(m.redoStack) >= maxUndo {
|
||||
copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:])
|
||||
m.redoStack = m.redoStack[:maxUndo]
|
||||
}
|
||||
m.d = m.undoStack[len(m.undoStack)-1]
|
||||
m.undoStack = m.undoStack[:len(m.undoStack)-1]
|
||||
m.prevUndoKind = ""
|
||||
(*Model)(m).send(m.d.Song.Copy())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Redo() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return len((*Model)(m).redoStack) > 0 },
|
||||
do: func() {
|
||||
m.undoStack = append(m.undoStack, m.d.Copy())
|
||||
if len(m.undoStack) >= maxUndo {
|
||||
copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:])
|
||||
m.undoStack = m.undoStack[:maxUndo]
|
||||
}
|
||||
m.d = m.redoStack[len(m.redoStack)-1]
|
||||
m.redoStack = m.redoStack[:len(m.redoStack)-1]
|
||||
m.prevUndoKind = ""
|
||||
(*Model)(m).send(m.d.Song.Copy())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddSemitone() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(1) })
|
||||
}
|
||||
|
||||
func (m *Model) SubtractSemitone() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(-1) })
|
||||
}
|
||||
|
||||
func (m *Model) AddOctave() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(12) })
|
||||
}
|
||||
|
||||
func (m *Model) SubtractOctave() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(-12) })
|
||||
}
|
||||
|
||||
func (m *Model) EditNoteOff() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Fill(0) })
|
||||
}
|
||||
|
||||
func (m *Model) RemoveUnused() Action {
|
||||
return Allow(func() {
|
||||
defer m.change("RemoveUnusedAction", ScoreChange, MajorChange)()
|
||||
for trkIndex, trk := range m.d.Song.Score.Tracks {
|
||||
// assign new indices to patterns
|
||||
newIndex := map[int]int{}
|
||||
runningIndex := 0
|
||||
length := 0
|
||||
if len(trk.Order) > m.d.Song.Score.Length {
|
||||
trk.Order = trk.Order[:m.d.Song.Score.Length]
|
||||
}
|
||||
for i, p := range trk.Order {
|
||||
// if the pattern hasn't been considered and is within limits
|
||||
if _, ok := newIndex[p]; !ok && p >= 0 && p < len(trk.Patterns) {
|
||||
pat := trk.Patterns[p]
|
||||
useful := false
|
||||
for _, n := range pat { // patterns that have anything else than all holds are useful and to be kept
|
||||
if n != 1 {
|
||||
useful = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if useful {
|
||||
newIndex[p] = runningIndex
|
||||
runningIndex++
|
||||
} else {
|
||||
newIndex[p] = -1
|
||||
}
|
||||
}
|
||||
if ind, ok := newIndex[p]; ok && ind > -1 {
|
||||
length = i + 1
|
||||
trk.Order[i] = ind
|
||||
} else {
|
||||
trk.Order[i] = -1
|
||||
}
|
||||
}
|
||||
trk.Order = trk.Order[:length]
|
||||
newPatterns := make([]sointu.Pattern, runningIndex)
|
||||
for i, pat := range trk.Patterns {
|
||||
if ind, ok := newIndex[i]; ok && ind > -1 {
|
||||
patLength := 0
|
||||
for j, note := range pat { // find last note that is something else that hold
|
||||
if note != 1 {
|
||||
patLength = j + 1
|
||||
}
|
||||
}
|
||||
if patLength > m.d.Song.Score.RowsPerPattern {
|
||||
patLength = m.d.Song.Score.RowsPerPattern
|
||||
}
|
||||
newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold
|
||||
}
|
||||
}
|
||||
trk.Patterns = newPatterns
|
||||
m.d.Song.Score.Tracks[trkIndex] = trk
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) Rewind() Action {
|
||||
return Action{
|
||||
allowed: func() bool {
|
||||
return m.playing || !m.instrEnlarged
|
||||
},
|
||||
do: func() {
|
||||
m.playing = true
|
||||
m.send(StartPlayMsg{})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddOrderRow(before bool) Action {
|
||||
return Allow(func() {
|
||||
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
||||
if before {
|
||||
m.d.Cursor.OrderRow++
|
||||
}
|
||||
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
||||
from := m.d.Cursor.OrderRow
|
||||
m.d.Song.Score.Length++
|
||||
for i := range m.d.Song.Score.Tracks {
|
||||
order := &m.d.Song.Score.Tracks[i].Order
|
||||
if len(*order) > from {
|
||||
*order = append(*order, -1)
|
||||
copy((*order)[from+1:], (*order)[from:])
|
||||
(*order)[from] = -1
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) DeleteOrderRow(backwards bool) Action {
|
||||
return Allow(func() {
|
||||
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
||||
from := m.d.Cursor.OrderRow
|
||||
m.d.Song.Score.Length--
|
||||
for i := range m.d.Song.Score.Tracks {
|
||||
order := &m.d.Song.Score.Tracks[i].Order
|
||||
if len(*order) > from {
|
||||
copy((*order)[from:], (*order)[from+1:])
|
||||
*order = (*order)[:len(*order)-1]
|
||||
}
|
||||
}
|
||||
if backwards {
|
||||
if m.d.Cursor.OrderRow > 0 {
|
||||
m.d.Cursor.OrderRow--
|
||||
}
|
||||
}
|
||||
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) NewSong() Action {
|
||||
return Allow(func() {
|
||||
m.dialog = NewSongChanges
|
||||
m.completeAction(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) OpenSong() Action {
|
||||
return Allow(func() {
|
||||
m.dialog = OpenSongChanges
|
||||
m.completeAction(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) Quit() Action {
|
||||
return Allow(func() {
|
||||
m.dialog = QuitChanges
|
||||
m.completeAction(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) ForceQuit() Action {
|
||||
return Allow(func() {
|
||||
m.quitted = true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) SaveSong() Action {
|
||||
return Allow(func() {
|
||||
if m.d.FilePath == "" {
|
||||
switch m.dialog {
|
||||
case NoDialog:
|
||||
m.dialog = SaveAsExplorer
|
||||
case NewSongChanges:
|
||||
m.dialog = NewSongSaveExplorer
|
||||
case OpenSongChanges:
|
||||
m.dialog = OpenSongSaveExplorer
|
||||
case QuitChanges:
|
||||
m.dialog = QuitSaveExplorer
|
||||
}
|
||||
return
|
||||
}
|
||||
f, err := os.Create(m.d.FilePath)
|
||||
if err != nil {
|
||||
m.Alerts().Add("Error creating file: "+err.Error(), Error)
|
||||
return
|
||||
}
|
||||
m.WriteSong(f)
|
||||
m.d.ChangedSinceSave = false
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) DiscardSong() Action { return Allow(func() { m.completeAction(false) }) }
|
||||
func (m *Model) SaveSongAs() Action { return Allow(func() { m.dialog = SaveAsExplorer }) }
|
||||
func (m *Model) Cancel() Action { return Allow(func() { m.dialog = NoDialog }) }
|
||||
func (m *Model) Export() Action { return Allow(func() { m.dialog = Export }) }
|
||||
func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) }
|
||||
func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) }
|
||||
|
||||
func (m *Model) completeAction(checkSave bool) {
|
||||
if checkSave && m.d.ChangedSinceSave {
|
||||
return
|
||||
}
|
||||
switch m.dialog {
|
||||
case NewSongChanges, NewSongSaveExplorer:
|
||||
c := m.change("NewSong", SongChange|LoopChange, MajorChange)
|
||||
m.resetSong()
|
||||
c()
|
||||
m.d.ChangedSinceSave = false
|
||||
m.dialog = NoDialog
|
||||
case OpenSongChanges, OpenSongSaveExplorer:
|
||||
m.dialog = OpenSongOpenExplorer
|
||||
case QuitChanges, QuitSaveExplorer:
|
||||
m.quitted = true
|
||||
m.dialog = NoDialog
|
||||
default:
|
||||
m.dialog = NoDialog
|
||||
}
|
||||
}
|
||||
110
tracker/alert.go
Normal file
110
tracker/alert.go
Normal file
@ -0,0 +1,110 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"time"
|
||||
)
|
||||
|
||||
const alertSpeed = 10 // units: fadeLevels per second
|
||||
const defaultAlertDuration = time.Second * 3
|
||||
|
||||
type (
|
||||
Alert struct {
|
||||
Name string
|
||||
Priority AlertPriority
|
||||
Message string
|
||||
Duration time.Duration
|
||||
FadeLevel float64
|
||||
}
|
||||
|
||||
AlertPriority int
|
||||
AlertYieldFunc func(alert Alert)
|
||||
Alerts Model
|
||||
)
|
||||
|
||||
const (
|
||||
None AlertPriority = iota
|
||||
Info
|
||||
Warning
|
||||
Error
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Alerts() *Alerts { return (*Alerts)(m) }
|
||||
|
||||
// Alerts methods
|
||||
|
||||
func (m *Alerts) Iterate(yield AlertYieldFunc) {
|
||||
for _, a := range m.alerts {
|
||||
yield(a)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Alerts) Update(d time.Duration) (animating bool) {
|
||||
for i := len(m.alerts) - 1; i >= 0; i-- {
|
||||
if m.alerts[i].Duration >= d {
|
||||
m.alerts[i].Duration -= d
|
||||
if m.alerts[i].FadeLevel < 1 {
|
||||
animating = true
|
||||
m.alerts[i].FadeLevel += float64(alertSpeed*d) / float64(time.Second)
|
||||
if m.alerts[i].FadeLevel > 1 {
|
||||
m.alerts[i].FadeLevel = 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.alerts[i].Duration = 0
|
||||
m.alerts[i].FadeLevel -= float64(alertSpeed*d) / float64(time.Second)
|
||||
animating = true
|
||||
if m.alerts[i].FadeLevel < 0 {
|
||||
heap.Remove(m, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Alerts) Add(message string, priority AlertPriority) {
|
||||
m.AddAlert(Alert{
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
Duration: defaultAlertDuration,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Alerts) AddNamed(name, message string, priority AlertPriority) {
|
||||
m.AddAlert(Alert{
|
||||
Name: name,
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
Duration: defaultAlertDuration,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Alerts) AddAlert(a Alert) {
|
||||
for i := range m.alerts {
|
||||
if n := m.alerts[i].Name; n != "" && n == a.Name {
|
||||
a.FadeLevel = m.alerts[i].FadeLevel
|
||||
m.alerts[i] = a
|
||||
heap.Fix(m, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
heap.Push(m, a)
|
||||
}
|
||||
|
||||
func (m *Alerts) Push(x any) {
|
||||
m.alerts = append(m.alerts, x.(Alert))
|
||||
}
|
||||
|
||||
func (m *Alerts) Pop() any {
|
||||
old := m.alerts
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
m.alerts = old[0 : n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func (m Alerts) Len() int { return len(m.alerts) }
|
||||
func (m Alerts) Less(i, j int) bool { return m.alerts[i].Priority < m.alerts[j].Priority }
|
||||
func (m Alerts) Swap(i, j int) { m.alerts[i], m.alerts[j] = m.alerts[j], m.alerts[i] }
|
||||
182
tracker/bool.go
Normal file
182
tracker/bool.go
Normal file
@ -0,0 +1,182 @@
|
||||
package tracker
|
||||
|
||||
type (
|
||||
Bool struct {
|
||||
BoolData
|
||||
}
|
||||
|
||||
BoolData interface {
|
||||
Value() bool
|
||||
Enabled() bool
|
||||
setValue(bool)
|
||||
}
|
||||
|
||||
Panic Model
|
||||
IsRecording Model
|
||||
Playing Model
|
||||
InstrEnlarged Model
|
||||
Effect Model
|
||||
CommentExpanded Model
|
||||
NoteTracking Model
|
||||
UnitSearching Model
|
||||
UnitDisabled Model
|
||||
LoopToggle Model
|
||||
)
|
||||
|
||||
func (v Bool) Toggle() {
|
||||
v.Set(!v.Value())
|
||||
}
|
||||
|
||||
func (v Bool) Set(value bool) {
|
||||
if v.Enabled() && v.Value() != value {
|
||||
v.setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Panic() *Panic { return (*Panic)(m) }
|
||||
func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
|
||||
func (m *Model) Playing() *Playing { return (*Playing)(m) }
|
||||
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
|
||||
func (m *Model) Effect() *Effect { return (*Effect)(m) }
|
||||
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
|
||||
func (m *Model) NoteTracking() *NoteTracking { return (*NoteTracking)(m) }
|
||||
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
|
||||
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
|
||||
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
|
||||
|
||||
// Panic methods
|
||||
|
||||
func (m *Panic) Bool() Bool { return Bool{m} }
|
||||
func (m *Panic) Value() bool { return m.panic }
|
||||
func (m *Panic) setValue(val bool) {
|
||||
m.panic = val
|
||||
(*Model)(m).send(PanicMsg{val})
|
||||
}
|
||||
func (m *Panic) Enabled() bool { return true }
|
||||
|
||||
// IsRecording methods
|
||||
|
||||
func (m *IsRecording) Bool() Bool { return Bool{m} }
|
||||
func (m *IsRecording) Value() bool { return (*Model)(m).recording }
|
||||
func (m *IsRecording) setValue(val bool) {
|
||||
m.recording = val
|
||||
m.instrEnlarged = val
|
||||
(*Model)(m).send(RecordingMsg{val})
|
||||
}
|
||||
func (m *IsRecording) Enabled() bool { return true }
|
||||
|
||||
// Playing methods
|
||||
|
||||
func (m *Playing) Bool() Bool { return Bool{m} }
|
||||
func (m *Playing) Value() bool { return m.playing }
|
||||
func (m *Playing) setValue(val bool) {
|
||||
m.playing = val
|
||||
if m.playing {
|
||||
(*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos})
|
||||
} else {
|
||||
(*Model)(m).send(IsPlayingMsg{val})
|
||||
}
|
||||
}
|
||||
func (m *Playing) Enabled() bool { return m.playing || !m.instrEnlarged }
|
||||
|
||||
// InstrEnlarged methods
|
||||
|
||||
func (m *InstrEnlarged) Bool() Bool { return Bool{m} }
|
||||
func (m *InstrEnlarged) Value() bool { return m.instrEnlarged }
|
||||
func (m *InstrEnlarged) setValue(val bool) { m.instrEnlarged = val }
|
||||
func (m *InstrEnlarged) Enabled() bool { return true }
|
||||
|
||||
// CommentExpanded methods
|
||||
|
||||
func (m *CommentExpanded) Bool() Bool { return Bool{m} }
|
||||
func (m *CommentExpanded) Value() bool { return m.commentExpanded }
|
||||
func (m *CommentExpanded) setValue(val bool) { m.commentExpanded = val }
|
||||
func (m *CommentExpanded) Enabled() bool { return true }
|
||||
|
||||
// NoteTracking methods
|
||||
|
||||
func (m *NoteTracking) Bool() Bool { return Bool{m} }
|
||||
func (m *NoteTracking) Value() bool { return m.playing && m.noteTracking }
|
||||
func (m *NoteTracking) setValue(val bool) { m.noteTracking = val }
|
||||
func (m *NoteTracking) Enabled() bool { return m.playing }
|
||||
|
||||
// Effect methods
|
||||
|
||||
func (m *Effect) Bool() Bool { return Bool{m} }
|
||||
func (m *Effect) Value() bool {
|
||||
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
||||
return false
|
||||
}
|
||||
return m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect
|
||||
}
|
||||
func (m *Effect) setValue(val bool) {
|
||||
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect = val
|
||||
}
|
||||
func (m *Effect) Enabled() bool { return true }
|
||||
|
||||
// UnitSearching methods
|
||||
|
||||
func (m *UnitSearching) Bool() Bool { return Bool{m} }
|
||||
func (m *UnitSearching) Value() bool { return m.d.UnitSearching }
|
||||
func (m *UnitSearching) setValue(val bool) {
|
||||
m.d.UnitSearching = val
|
||||
if !val {
|
||||
m.d.UnitSearchString = ""
|
||||
}
|
||||
}
|
||||
func (m *UnitSearching) Enabled() bool { return true }
|
||||
|
||||
// UnitDisabled methods
|
||||
|
||||
func (m *UnitDisabled) Bool() Bool { return Bool{m} }
|
||||
func (m *UnitDisabled) Value() bool {
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
if m.d.UnitIndex < 0 || m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||
return false
|
||||
}
|
||||
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Disabled
|
||||
}
|
||||
func (m *UnitDisabled) setValue(val bool) {
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
l := ((*Model)(m)).Units().List()
|
||||
a, b := l.listRange()
|
||||
defer (*Model)(m).change("UnitDisabledSet", PatchChange, MajorChange)()
|
||||
for i := a; i <= b; i++ {
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[i].Disabled = val
|
||||
}
|
||||
}
|
||||
func (m *UnitDisabled) Enabled() bool {
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
if len(m.d.Song.Patch[m.d.InstrIndex].Units) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// LoopToggle methods
|
||||
|
||||
func (m *LoopToggle) Bool() Bool { return Bool{m} }
|
||||
func (m *LoopToggle) Value() bool { return m.d.Loop.Length > 0 }
|
||||
func (t *LoopToggle) setValue(val bool) {
|
||||
m := (*Model)(t)
|
||||
defer m.change("SetLoopAction", LoopChange, MinorChange)()
|
||||
if !val {
|
||||
m.d.Loop = Loop{}
|
||||
return
|
||||
}
|
||||
l := m.OrderRows().List()
|
||||
a, b := l.listRange()
|
||||
m.d.Loop = Loop{a, b - a + 1}
|
||||
}
|
||||
func (m *LoopToggle) Enabled() bool { return true }
|
||||
179
tracker/files.go
Normal file
179
tracker/files.go
Normal file
@ -0,0 +1,179 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
func (m *Model) ReadSong(r io.ReadCloser) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
f := m.change("LoadSong", SongChange, MajorChange)
|
||||
m.d.Song = song
|
||||
if f, ok := r.(*os.File); ok {
|
||||
m.d.FilePath = f.Name()
|
||||
// when the song is loaded from a file, we are quite confident that the file is persisted and thus
|
||||
// we can close sointu without worrying about losing changes
|
||||
m.d.ChangedSinceSave = false
|
||||
}
|
||||
f()
|
||||
m.completeAction(false)
|
||||
}
|
||||
|
||||
func (m *Model) WriteSong(w io.WriteCloser) {
|
||||
path := ""
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(m.d.Song)
|
||||
} else {
|
||||
contents, err = yaml.Marshal(m.d.Song)
|
||||
}
|
||||
if err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error marshaling a song file: %v", err), Error)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(contents); err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error writing to file: %v", err), Error)
|
||||
return
|
||||
}
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
// when the song is saved to a file, we are quite confident that the file is persisted and thus
|
||||
// we can close sointu without worrying about losing changes
|
||||
m.d.ChangedSinceSave = false
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
|
||||
return
|
||||
}
|
||||
m.d.FilePath = path
|
||||
m.completeAction(false)
|
||||
}
|
||||
|
||||
func (m *Model) WriteWav(w io.WriteCloser, pcm16 bool, execChan chan<- func()) {
|
||||
m.dialog = NoDialog
|
||||
song := m.d.Song.Copy()
|
||||
go func() {
|
||||
b := make([]byte, 32+2)
|
||||
rand.Read(b)
|
||||
name := fmt.Sprintf("%x", b)[2 : 32+2]
|
||||
data, err := sointu.Play(m.synther, song, func(p float32) {
|
||||
execChan <- func() {
|
||||
m.Alerts().AddNamed(name, fmt.Sprintf("Exporting song: %.0f%%", p*100), Info)
|
||||
}
|
||||
}) // render the song to calculate its length
|
||||
if err != nil {
|
||||
execChan <- func() {
|
||||
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
|
||||
}
|
||||
return
|
||||
}
|
||||
buffer, err := data.Wav(pcm16)
|
||||
if err != nil {
|
||||
execChan <- func() {
|
||||
m.Alerts().Add(fmt.Sprintf("Error converting to .wav: %v", err), Error)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.Write(buffer)
|
||||
w.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Model) SaveInstrument(w io.WriteCloser) bool {
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
m.Alerts().Add("No instrument selected", Error)
|
||||
return false
|
||||
}
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(m.d.Song.Patch[m.d.InstrIndex])
|
||||
} else {
|
||||
contents, err = yaml.Marshal(m.d.Song.Patch[m.d.InstrIndex])
|
||||
}
|
||||
if err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error)
|
||||
return false
|
||||
}
|
||||
w.Write(contents)
|
||||
w.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) LoadInstrument(r io.ReadCloser) bool {
|
||||
if m.d.InstrIndex < 0 {
|
||||
return false
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var instrument sointu.Instrument
|
||||
var errJSON, errYaml, err4ki, err4kp error
|
||||
var patch sointu.Patch
|
||||
errJSON = json.Unmarshal(b, &instrument)
|
||||
if errJSON == nil {
|
||||
goto success
|
||||
}
|
||||
errYaml = yaml.Unmarshal(b, &instrument)
|
||||
if errYaml == nil {
|
||||
goto success
|
||||
}
|
||||
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||
if err4kp == nil {
|
||||
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
||||
m.d.Song.Patch = patch
|
||||
return true
|
||||
}
|
||||
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||
if err4ki == nil {
|
||||
goto success
|
||||
}
|
||||
m.Alerts().Add(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error)
|
||||
return false
|
||||
success:
|
||||
if f, ok := r.(*os.File); ok {
|
||||
filename := f.Name()
|
||||
// the 4klang instrument names are junk, replace them with the filename without extension
|
||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||
}
|
||||
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
||||
for len(m.d.Song.Patch) <= m.d.InstrIndex {
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||
}
|
||||
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
||||
if m.d.Song.Patch[m.d.InstrIndex].Comment != "" {
|
||||
m.commentExpanded = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
message string
|
||||
alertType AlertType
|
||||
duration time.Duration
|
||||
showMessage string
|
||||
showAlertType AlertType
|
||||
showDuration time.Duration
|
||||
showTime time.Time
|
||||
pos float64
|
||||
lastUpdate time.Time
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
type AlertType int
|
||||
|
||||
const (
|
||||
None AlertType = iota
|
||||
Notify
|
||||
Warning
|
||||
Error
|
||||
)
|
||||
|
||||
var alertSpeed = 150 * time.Millisecond
|
||||
var alertMargin = layout.UniformInset(unit.Dp(6))
|
||||
var alertInset = layout.UniformInset(unit.Dp(6))
|
||||
|
||||
func (a *Alert) Update(message string, alertType AlertType, duration time.Duration) {
|
||||
if a.alertType < alertType {
|
||||
a.message = message
|
||||
a.alertType = alertType
|
||||
a.duration = duration
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Alert) Layout(gtx C) D {
|
||||
now := time.Now()
|
||||
if a.alertType != None {
|
||||
a.showMessage = a.message
|
||||
a.showAlertType = a.alertType
|
||||
a.showTime = now
|
||||
a.showDuration = a.duration
|
||||
}
|
||||
a.alertType = None
|
||||
var targetPos float64 = 0.0
|
||||
if now.Sub(a.showTime) <= a.showDuration {
|
||||
targetPos = 1.0
|
||||
}
|
||||
delta := float64(now.Sub(a.lastUpdate)) / float64(alertSpeed)
|
||||
if a.pos < targetPos {
|
||||
a.pos += delta
|
||||
if a.pos > targetPos {
|
||||
a.pos = targetPos
|
||||
} else {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
} else if a.pos > targetPos {
|
||||
a.pos -= delta
|
||||
if a.pos < targetPos {
|
||||
a.pos = targetPos
|
||||
} else {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
a.lastUpdate = now
|
||||
var color, textColor, shadeColor color.NRGBA
|
||||
switch a.showAlertType {
|
||||
case Warning:
|
||||
color = warningColor
|
||||
textColor = black
|
||||
case Error:
|
||||
color = errorColor
|
||||
textColor = black
|
||||
default:
|
||||
color = popupSurfaceColor
|
||||
textColor = white
|
||||
shadeColor = black
|
||||
}
|
||||
bgWidget := func(gtx C) D {
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: a.showMessage, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
|
||||
return alertMargin.Layout(gtx, func(gtx C) D {
|
||||
return layout.S.Layout(gtx, func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
recording := op.Record(gtx.Ops)
|
||||
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(bgWidget),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return alertInset.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
)
|
||||
macro := recording.Stop()
|
||||
totalY := dims.Size.Y + gtx.Dp(alertMargin.Bottom)
|
||||
op.Offset(image.Point{0, int((1 - a.pos) * float64(totalY))}).Add((gtx.Ops))
|
||||
macro.Add(gtx.Ops)
|
||||
return dims
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -6,17 +6,43 @@ import (
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type TipClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
type (
|
||||
TipClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
}
|
||||
|
||||
ActionClickable struct {
|
||||
Action tracker.Action
|
||||
TipClickable
|
||||
}
|
||||
|
||||
TipIconButtonStyle struct {
|
||||
TipArea *component.TipArea
|
||||
IconButtonStyle material.IconButtonStyle
|
||||
Tooltip component.Tooltip
|
||||
}
|
||||
|
||||
BoolClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
Bool tracker.Bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewActionClickable(a tracker.Action) *ActionClickable {
|
||||
return &ActionClickable{
|
||||
Action: a,
|
||||
}
|
||||
}
|
||||
|
||||
type TipIconButtonStyle struct {
|
||||
IconButtonStyle material.IconButtonStyle
|
||||
Tooltip component.Tooltip
|
||||
tipArea *component.TipArea
|
||||
func NewBoolClickable(b tracker.Bool) *BoolClickable {
|
||||
return &BoolClickable{
|
||||
Bool: b,
|
||||
}
|
||||
}
|
||||
|
||||
func Tooltip(th *material.Theme, tip string) component.Tooltip {
|
||||
@ -25,24 +51,86 @@ func Tooltip(th *material.Theme, tip string) component.Tooltip {
|
||||
return tooltip
|
||||
}
|
||||
|
||||
func IconButton(th *material.Theme, w *TipClickable, icon []byte, enabled bool, tip string) TipIconButtonStyle {
|
||||
ret := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if enabled {
|
||||
ret.Color = primaryColor
|
||||
} else {
|
||||
ret.Color = disabledTextColor
|
||||
func ActionIcon(gtx C, th *material.Theme, w *ActionClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||
ret := TipIcon(th, &w.TipClickable, icon, tip)
|
||||
for w.Clickable.Clicked(gtx) {
|
||||
w.Action.Do()
|
||||
}
|
||||
if !w.Action.Allowed() {
|
||||
ret.IconButtonStyle.Color = disabledTextColor
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TipIcon(th *material.Theme, w *TipClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||
iconButtonStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
iconButtonStyle.Color = primaryColor
|
||||
iconButtonStyle.Background = transparent
|
||||
iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
return TipIconButtonStyle{
|
||||
TipArea: &w.TipArea,
|
||||
IconButtonStyle: iconButtonStyle,
|
||||
Tooltip: Tooltip(th, tip),
|
||||
}
|
||||
}
|
||||
|
||||
func ToggleIcon(gtx C, th *material.Theme, w *BoolClickable, offIcon, onIcon []byte, offTip, onTip string) TipIconButtonStyle {
|
||||
icon := offIcon
|
||||
tip := offTip
|
||||
if w.Bool.Value() {
|
||||
icon = onIcon
|
||||
tip = onTip
|
||||
}
|
||||
for w.Clickable.Clicked(gtx) {
|
||||
w.Bool.Toggle()
|
||||
}
|
||||
ibStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
ibStyle.Background = transparent
|
||||
ibStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
ibStyle.Color = primaryColor
|
||||
if !w.Bool.Enabled() {
|
||||
ibStyle.Color = disabledTextColor
|
||||
}
|
||||
return TipIconButtonStyle{
|
||||
IconButtonStyle: ret,
|
||||
TipArea: &w.TipArea,
|
||||
IconButtonStyle: ibStyle,
|
||||
Tooltip: Tooltip(th, tip),
|
||||
tipArea: &w.TipArea,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TipIconButtonStyle) Layout(gtx C) D {
|
||||
return t.tipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||
return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||
}
|
||||
|
||||
func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) material.ButtonStyle {
|
||||
for w.Clickable.Clicked(gtx) {
|
||||
w.Action.Do()
|
||||
}
|
||||
ret := material.Button(th, &w.Clickable, text)
|
||||
ret.Color = th.Palette.Fg
|
||||
if !w.Action.Allowed() {
|
||||
ret.Color = disabledTextColor
|
||||
}
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
return ret
|
||||
}
|
||||
|
||||
func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) material.ButtonStyle {
|
||||
for b.Clickable.Clicked(gtx) {
|
||||
b.Bool.Toggle()
|
||||
}
|
||||
ret := material.Button(th, &b.Clickable, text)
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if b.Bool.Value() {
|
||||
ret.Color = th.Palette.ContrastFg
|
||||
ret.Background = th.Palette.Fg
|
||||
} else {
|
||||
ret.Color = th.Palette.Fg
|
||||
ret.Background = transparent
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func LowEmphasisButton(th *material.Theme, w *widget.Clickable, text string) material.ButtonStyle {
|
||||
|
||||
@ -1,56 +1,116 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
Visible bool
|
||||
BtnAlt widget.Clickable
|
||||
BtnOk widget.Clickable
|
||||
BtnCancel widget.Clickable
|
||||
BtnAlt *ActionClickable
|
||||
BtnOk *ActionClickable
|
||||
BtnCancel *ActionClickable
|
||||
tag bool
|
||||
keyFilters []event.Filter
|
||||
}
|
||||
|
||||
type DialogStyle struct {
|
||||
dialog *Dialog
|
||||
Title string
|
||||
Text string
|
||||
Inset layout.Inset
|
||||
ShowAlt bool
|
||||
TextInset layout.Inset
|
||||
AltStyle material.ButtonStyle
|
||||
OkStyle material.ButtonStyle
|
||||
CancelStyle material.ButtonStyle
|
||||
Shaper *text.Shaper
|
||||
}
|
||||
|
||||
func ConfirmDialog(th *material.Theme, dialog *Dialog, text string, shaper *text.Shaper) DialogStyle {
|
||||
func NewDialog(ok, alt, cancel tracker.Action) *Dialog {
|
||||
ret := &Dialog{
|
||||
BtnOk: NewActionClickable(ok),
|
||||
BtnAlt: NewActionClickable(alt),
|
||||
BtnCancel: NewActionClickable(cancel),
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func ConfirmDialog(gtx C, th *material.Theme, dialog *Dialog, title, text string) DialogStyle {
|
||||
ret := DialogStyle{
|
||||
dialog: dialog,
|
||||
Title: title,
|
||||
Text: text,
|
||||
Inset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12), Left: unit.Dp(20), Right: unit.Dp(20)},
|
||||
AltStyle: HighEmphasisButton(th, &dialog.BtnAlt, "Alt"),
|
||||
OkStyle: HighEmphasisButton(th, &dialog.BtnOk, "Ok"),
|
||||
CancelStyle: HighEmphasisButton(th, &dialog.BtnCancel, "Cancel"),
|
||||
Shaper: shaper,
|
||||
TextInset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12)},
|
||||
AltStyle: ActionButton(gtx, th, dialog.BtnAlt, "Alt"),
|
||||
OkStyle: ActionButton(gtx, th, dialog.BtnOk, "Ok"),
|
||||
CancelStyle: ActionButton(gtx, th, dialog.BtnCancel, "Cancel"),
|
||||
Shaper: th.Shaper,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *ActionClickable) {
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.Filter{Focus: &btn.Clickable, Name: key.NameLeftArrow},
|
||||
key.Filter{Focus: &btn.Clickable, Name: key.NameRightArrow},
|
||||
key.Filter{Focus: &btn.Clickable, Name: key.NameEscape},
|
||||
key.Filter{Focus: &btn.Clickable, Name: key.NameTab, Optional: key.ModShift},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
switch {
|
||||
case e.Name == key.NameLeftArrow || (e.Name == key.NameTab && e.Modifiers.Contain(key.ModShift)):
|
||||
gtx.Execute(key.FocusCmd{Tag: &prev.Clickable})
|
||||
case e.Name == key.NameRightArrow || (e.Name == key.NameTab && !e.Modifiers.Contain(key.ModShift)):
|
||||
gtx.Execute(key.FocusCmd{Tag: &next.Clickable})
|
||||
case e.Name == key.NameEscape:
|
||||
d.BtnCancel.Action.Do()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialog) handleKeys(gtx C) {
|
||||
if d.BtnAlt.Action.Allowed() {
|
||||
d.handleKeysForButton(gtx, d.BtnAlt, d.BtnCancel, d.BtnOk)
|
||||
d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnAlt)
|
||||
d.handleKeysForButton(gtx, d.BtnOk, d.BtnAlt, d.BtnCancel)
|
||||
} else {
|
||||
d.handleKeysForButton(gtx, d.BtnOk, d.BtnCancel, d.BtnCancel)
|
||||
d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnOk)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DialogStyle) Layout(gtx C) D {
|
||||
if d.dialog.Visible {
|
||||
paint.Fill(gtx.Ops, dialogBgColor)
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return Popup(&d.dialog.Visible).Layout(gtx, func(gtx C) D {
|
||||
return d.Inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label(d.Text, highEmphasisTextColor, d.Shaper)),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !gtx.Source.Focused(&d.dialog.BtnOk.Clickable) && !gtx.Source.Focused(&d.dialog.BtnCancel.Clickable) && !gtx.Source.Focused(&d.dialog.BtnAlt.Clickable) {
|
||||
gtx.Execute(key.FocusCmd{Tag: &d.dialog.BtnCancel.Clickable})
|
||||
}
|
||||
d.dialog.handleKeys(gtx)
|
||||
paint.Fill(gtx.Ops, dialogBgColor)
|
||||
text := func(gtx C) D {
|
||||
return d.TextInset.Layout(gtx, LabelStyle{Text: d.Text, Color: highEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(14), Shaper: d.Shaper}.Layout)
|
||||
}
|
||||
visible := true
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return Popup(&visible).Layout(gtx, func(gtx C) D {
|
||||
return d.Inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label(d.Title, highEmphasisTextColor, d.Shaper)),
|
||||
layout.Rigid(text),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(120))
|
||||
if d.ShowAlt {
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
|
||||
layout.Rigid(d.OkStyle.Layout),
|
||||
layout.Rigid(d.AltStyle.Layout),
|
||||
@ -61,11 +121,10 @@ func (d *DialogStyle) Layout(gtx C) D {
|
||||
layout.Rigid(d.OkStyle.Layout),
|
||||
layout.Rigid(d.CancelStyle.Layout),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
return D{}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,51 +1,60 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type DragList struct {
|
||||
SelectedItem int
|
||||
SelectedItem2 int
|
||||
HoverItem int
|
||||
List *layout.List
|
||||
drag bool
|
||||
dragID pointer.ID
|
||||
tags []bool
|
||||
swapped bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
mainTag bool
|
||||
TrackerList tracker.List
|
||||
HoverItem int
|
||||
List *layout.List
|
||||
ScrollBar *ScrollBar
|
||||
drag bool
|
||||
dragID pointer.ID
|
||||
tags []bool
|
||||
swapped bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
type FilledDragListStyle struct {
|
||||
dragList *DragList
|
||||
HoverColor color.NRGBA
|
||||
SelectedColor color.NRGBA
|
||||
CursorColor color.NRGBA
|
||||
Count int
|
||||
element func(gtx C, i int) D
|
||||
swap func(i, j int)
|
||||
dragList *DragList
|
||||
HoverColor color.NRGBA
|
||||
SelectedColor color.NRGBA
|
||||
CursorColor color.NRGBA
|
||||
ScrollBarWidth unit.Dp
|
||||
element, bg func(gtx C, i int) D
|
||||
}
|
||||
|
||||
func FilledDragList(th *material.Theme, dragList *DragList, count int, element func(gtx C, i int) D, swap func(i, j int)) FilledDragListStyle {
|
||||
func NewDragList(model tracker.List, axis layout.Axis) *DragList {
|
||||
return &DragList{TrackerList: model, List: &layout.List{Axis: axis}, HoverItem: -1, ScrollBar: &ScrollBar{Axis: axis}}
|
||||
}
|
||||
|
||||
func FilledDragList(th *material.Theme, dragList *DragList, element, bg func(gtx C, i int) D) FilledDragListStyle {
|
||||
return FilledDragListStyle{
|
||||
dragList: dragList,
|
||||
element: element,
|
||||
swap: swap,
|
||||
Count: count,
|
||||
HoverColor: dragListHoverColor,
|
||||
SelectedColor: dragListSelectedColor,
|
||||
CursorColor: cursorColor,
|
||||
dragList: dragList,
|
||||
element: element,
|
||||
bg: bg,
|
||||
HoverColor: dragListHoverColor,
|
||||
SelectedColor: dragListSelectedColor,
|
||||
CursorColor: cursorColor,
|
||||
ScrollBarWidth: unit.Dp(10),
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,16 +66,16 @@ func (d *DragList) Focused() bool {
|
||||
return d.focused
|
||||
}
|
||||
|
||||
func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
func (s FilledDragListStyle) LayoutScrollBar(gtx C) D {
|
||||
return s.dragList.ScrollBar.Layout(gtx, s.ScrollBarWidth, s.dragList.TrackerList.Count(), &s.dragList.List.Position)
|
||||
}
|
||||
|
||||
func (s FilledDragListStyle) Layout(gtx C) D {
|
||||
swap := 0
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓")
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→")
|
||||
}
|
||||
key.InputOp{Tag: &s.dragList.mainTag, Keys: keys}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, s.dragList)
|
||||
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
@ -76,74 +85,92 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
|
||||
if s.dragList.requestFocus {
|
||||
s.dragList.requestFocus = false
|
||||
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
||||
gtx.Execute(key.FocusCmd{Tag: s.dragList})
|
||||
}
|
||||
|
||||
if !s.dragList.focused {
|
||||
s.dragList.SelectedItem2 = s.dragList.SelectedItem
|
||||
prevKey := key.NameUpArrow
|
||||
nextKey := key.NameDownArrow
|
||||
firstKey := key.NamePageUp
|
||||
lastKey := key.NamePageDown
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
prevKey = key.NameLeftArrow
|
||||
nextKey = key.NameRightArrow
|
||||
firstKey = key.NameHome
|
||||
lastKey = key.NameEnd
|
||||
}
|
||||
|
||||
for _, ke := range gtx.Events(&s.dragList.mainTag) {
|
||||
switch ke := ke.(type) {
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.FocusFilter{Target: s.dragList},
|
||||
transfer.TargetFilter{Target: s.dragList, Type: "application/text"},
|
||||
key.Filter{Focus: s.dragList, Name: prevKey, Optional: key.ModShift | key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: nextKey, Optional: key.ModShift | key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: firstKey, Optional: key.ModShift | key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: lastKey, Optional: key.ModShift | key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: "A", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: "C", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: "X", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: "V", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: key.NameDeleteBackward, Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.dragList, Name: key.NameDeleteForward},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch ke := event.(type) {
|
||||
case key.FocusEvent:
|
||||
s.dragList.focused = ke.Focus
|
||||
if !s.dragList.focused {
|
||||
s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected())
|
||||
}
|
||||
case key.Event:
|
||||
if !s.dragList.focused || ke.State != key.Press {
|
||||
break
|
||||
}
|
||||
delta := 0
|
||||
switch {
|
||||
case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameLeftArrow && s.dragList.SelectedItem > 0:
|
||||
delta = -1
|
||||
case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameRightArrow && s.dragList.SelectedItem < s.Count-1:
|
||||
delta = 1
|
||||
case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameUpArrow && s.dragList.SelectedItem > 0:
|
||||
delta = -1
|
||||
case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameDownArrow && s.dragList.SelectedItem < s.Count-1:
|
||||
delta = 1
|
||||
}
|
||||
if delta != 0 {
|
||||
if ke.Modifiers.Contain(key.ModShortcut) {
|
||||
swap = delta
|
||||
} else {
|
||||
s.dragList.SelectedItem += delta
|
||||
if !ke.Modifiers.Contain(key.ModShift) {
|
||||
s.dragList.SelectedItem2 = s.dragList.SelectedItem
|
||||
}
|
||||
}
|
||||
s.dragList.command(gtx, ke)
|
||||
case transfer.DataEvent:
|
||||
if b, err := io.ReadAll(ke.Open()); err == nil {
|
||||
s.dragList.TrackerList.PasteElements([]byte(b))
|
||||
}
|
||||
|
||||
}
|
||||
gtx.Execute(op.InvalidateCmd{})
|
||||
}
|
||||
|
||||
_, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData)
|
||||
|
||||
listElem := func(gtx C, index int) D {
|
||||
for len(s.dragList.tags) <= index {
|
||||
s.dragList.tags = append(s.dragList.tags, false)
|
||||
}
|
||||
bg := func(gtx C) D {
|
||||
cursorBg := func(gtx C) D {
|
||||
var color color.NRGBA
|
||||
if s.dragList.SelectedItem == index {
|
||||
if s.dragList.TrackerList.Selected() == index {
|
||||
if s.dragList.focused {
|
||||
color = s.CursorColor
|
||||
} else {
|
||||
color = s.SelectedColor
|
||||
}
|
||||
} else if between(s.dragList.SelectedItem, index, s.dragList.SelectedItem2) {
|
||||
} else if between(s.dragList.TrackerList.Selected(), index, s.dragList.TrackerList.Selected2()) {
|
||||
color = s.SelectedColor
|
||||
} else if s.dragList.HoverItem == index {
|
||||
color = s.HoverColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
|
||||
inputFg := func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
for _, ev := range gtx.Events(&s.dragList.tags[index]) {
|
||||
for {
|
||||
ev, ok := gtx.Event(pointer.Filter{
|
||||
Target: &s.dragList.tags[index],
|
||||
Kinds: pointer.Press | pointer.Enter | pointer.Leave,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Enter:
|
||||
s.dragList.HoverItem = index
|
||||
case pointer.Leave:
|
||||
@ -154,26 +181,32 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
if s.dragList.drag {
|
||||
break
|
||||
}
|
||||
s.dragList.SelectedItem = index
|
||||
s.dragList.TrackerList.SetSelected(index)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.dragList.SelectedItem2 = index
|
||||
s.dragList.TrackerList.SetSelected2(index)
|
||||
}
|
||||
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
||||
gtx.Execute(key.FocusCmd{Tag: s.dragList})
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &s.dragList.tags[index],
|
||||
Types: pointer.Press | pointer.Enter | pointer.Leave,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &s.dragList.tags[index])
|
||||
area.Pop()
|
||||
if index == s.dragList.SelectedItem {
|
||||
for _, ev := range gtx.Events(&s.dragList.focused) {
|
||||
if index == s.dragList.TrackerList.Selected() && isMutable {
|
||||
for {
|
||||
target := &s.dragList.focused
|
||||
if s.dragList.drag {
|
||||
target = nil
|
||||
}
|
||||
ev, ok := gtx.Event(pointer.Filter{Target: target, Kinds: pointer.Drag | pointer.Press | pointer.Release | pointer.Cancel})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
s.dragList.dragID = e.PointerID
|
||||
s.dragList.drag = true
|
||||
@ -196,45 +229,42 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
swap = 1
|
||||
}
|
||||
}
|
||||
case pointer.Release:
|
||||
fallthrough
|
||||
case pointer.Cancel:
|
||||
case pointer.Release, pointer.Cancel:
|
||||
s.dragList.drag = false
|
||||
}
|
||||
}
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &s.dragList.focused,
|
||||
Types: pointer.Drag | pointer.Press | pointer.Release,
|
||||
Grab: s.dragList.drag,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &s.dragList.focused)
|
||||
pointer.CursorGrab.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Expanded(inputFg),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return s.element(gtx, index)
|
||||
}),
|
||||
)
|
||||
}
|
||||
dims := s.dragList.List.Layout(gtx, s.Count, listElem)
|
||||
a := intMin(s.dragList.SelectedItem, s.dragList.SelectedItem2)
|
||||
b := intMax(s.dragList.SelectedItem, s.dragList.SelectedItem2)
|
||||
if !s.dragList.swapped && swap != 0 && a+swap >= 0 && b+swap < s.Count {
|
||||
if swap < 0 {
|
||||
for i := a; i <= b; i++ {
|
||||
s.swap(i, i+swap)
|
||||
}
|
||||
} else {
|
||||
for i := b; i >= a; i-- {
|
||||
s.swap(i, i+swap)
|
||||
}
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := s.element(gtx, index)
|
||||
call := macro.Stop()
|
||||
gtx.Constraints.Min = dims.Size
|
||||
if s.bg != nil {
|
||||
s.bg(gtx, index)
|
||||
}
|
||||
cursorBg(gtx)
|
||||
call.Add(gtx.Ops)
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
dims.Size.Y = gtx.Constraints.Max.Y
|
||||
} else {
|
||||
dims.Size.X = gtx.Constraints.Max.X
|
||||
}
|
||||
return dims
|
||||
}
|
||||
count := s.dragList.TrackerList.Count()
|
||||
if count < 1 {
|
||||
count = 1 // draw at least one empty element to get the correct size
|
||||
}
|
||||
dims := s.dragList.List.Layout(gtx, count, listElem)
|
||||
if !s.dragList.swapped && swap != 0 {
|
||||
if s.dragList.TrackerList.MoveElements(swap) {
|
||||
gtx.Execute(op.InvalidateCmd{})
|
||||
}
|
||||
s.dragList.SelectedItem += swap
|
||||
s.dragList.SelectedItem2 += swap
|
||||
s.dragList.swapped = true
|
||||
} else {
|
||||
s.dragList.swapped = false
|
||||
@ -242,6 +272,88 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
return dims
|
||||
}
|
||||
|
||||
func (e *DragList) command(gtx layout.Context, k key.Event) {
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
switch k.Name {
|
||||
case "V":
|
||||
gtx.Execute(clipboard.ReadCmd{Tag: e})
|
||||
return
|
||||
case "C", "X":
|
||||
data, ok := e.TrackerList.CopyElements()
|
||||
if ok && (k.Name == "C" || e.TrackerList.DeleteElements(false)) {
|
||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(data))})
|
||||
}
|
||||
return
|
||||
case "A":
|
||||
e.TrackerList.SetSelected(0)
|
||||
e.TrackerList.SetSelected2(e.TrackerList.Count() - 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
delta := 0
|
||||
switch k.Name {
|
||||
case key.NameDeleteBackward:
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
e.TrackerList.DeleteElements(true)
|
||||
}
|
||||
return
|
||||
case key.NameDeleteForward:
|
||||
e.TrackerList.DeleteElements(false)
|
||||
return
|
||||
case key.NameLeftArrow:
|
||||
delta = -1
|
||||
case key.NameRightArrow:
|
||||
delta = 1
|
||||
case key.NameHome:
|
||||
delta = -1e6
|
||||
case key.NameEnd:
|
||||
delta = 1e6
|
||||
case key.NameUpArrow:
|
||||
delta = -1
|
||||
case key.NameDownArrow:
|
||||
delta = 1
|
||||
case key.NamePageUp:
|
||||
delta = -1e6
|
||||
case key.NamePageDown:
|
||||
delta = 1e6
|
||||
}
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
e.TrackerList.MoveElements(delta)
|
||||
} else {
|
||||
e.TrackerList.SetSelected(e.TrackerList.Selected() + delta)
|
||||
if !k.Modifiers.Contain(key.ModShift) {
|
||||
e.TrackerList.SetSelected2(e.TrackerList.Selected())
|
||||
}
|
||||
}
|
||||
e.EnsureVisible(e.TrackerList.Selected())
|
||||
}
|
||||
|
||||
func (l *DragList) EnsureVisible(item int) {
|
||||
first := l.List.Position.First
|
||||
last := l.List.Position.First + l.List.Position.Count - 1
|
||||
if item < first || (item == first && l.List.Position.Offset > 0) {
|
||||
l.List.ScrollTo(item)
|
||||
}
|
||||
if item > last || (item == last && l.List.Position.OffsetLast < 0) {
|
||||
o := -l.List.Position.OffsetLast + l.List.Position.Offset
|
||||
l.List.ScrollTo(item - l.List.Position.Count + 1)
|
||||
l.List.Position.Offset = o
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DragList) CenterOn(item int) {
|
||||
lenPerChildPx := l.List.Position.Length / l.TrackerList.Count()
|
||||
if lenPerChildPx == 0 {
|
||||
return
|
||||
}
|
||||
listLengthPx := l.List.Position.Count*l.List.Position.Length/l.TrackerList.Count() + l.List.Position.OffsetLast - l.List.Position.Offset
|
||||
lenBeforeItem := (listLengthPx - lenPerChildPx) / 2
|
||||
quot := lenBeforeItem / lenPerChildPx
|
||||
rem := lenBeforeItem % lenPerChildPx
|
||||
l.List.ScrollTo(item - quot - 1)
|
||||
l.List.Position.Offset = lenPerChildPx - rem
|
||||
}
|
||||
|
||||
func between(a, b, c int) bool {
|
||||
return (a <= b && b <= c) || (c <= b && b <= a)
|
||||
}
|
||||
|
||||
@ -1,224 +0,0 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
func (t *Tracker) OpenSongFile(forced bool) {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmLoad
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return
|
||||
}
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.loadSong(reader)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongFile() bool {
|
||||
if p := t.FilePath(); p != "" {
|
||||
if f, err := os.Create(p); err == nil {
|
||||
return t.saveSong(f)
|
||||
}
|
||||
}
|
||||
t.SaveSongAsFile()
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongAsFile() {
|
||||
p := t.FilePath()
|
||||
if p == "" {
|
||||
p = "song.yml"
|
||||
}
|
||||
writer, err := t.Explorer.CreateFile(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.saveSong(writer)
|
||||
}
|
||||
|
||||
func (t *Tracker) ExportWav(pcm16 bool) {
|
||||
filename := "song.wav"
|
||||
if p := t.FilePath(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
writer, err := t.Explorer.CreateFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.exportWav(writer, pcm16)
|
||||
}
|
||||
|
||||
func (t *Tracker) LoadInstrument() {
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.loadInstrument(reader)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveInstrument() {
|
||||
writer, err := t.Explorer.CreateFile(t.Instrument().Name + ".yml")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.saveInstrument(writer)
|
||||
}
|
||||
|
||||
func (t *Tracker) loadSong(r io.ReadCloser) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3)
|
||||
}
|
||||
}
|
||||
if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 {
|
||||
t.Alert.Update("The song file is malformed", Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
t.SetSong(song)
|
||||
path := ""
|
||||
if f, ok := r.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
t.SetFilePath(path)
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
func (t *Tracker) saveSong(w io.WriteCloser) bool {
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(t.Song())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.Song())
|
||||
}
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
if _, err := w.Write(contents); err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error writing to file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error closing file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
t.SetFilePath(path)
|
||||
t.SetChangedSinceSave(false)
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tracker) exportWav(w io.WriteCloser, pcm16 bool) {
|
||||
data, err := sointu.Play(t.synther, t.Song()) // render the song to calculate its length
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
buffer, err := data.Wav(pcm16)
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
w.Write(buffer)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func (t *Tracker) saveInstrument(w io.WriteCloser) bool {
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(t.Instrument())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.Instrument())
|
||||
}
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
w.Write(contents)
|
||||
w.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tracker) loadInstrument(r io.ReadCloser) bool {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var instrument sointu.Instrument
|
||||
var errJSON, errYaml, err4ki, err4kp error
|
||||
var patch sointu.Patch
|
||||
errJSON = json.Unmarshal(b, &instrument)
|
||||
if errJSON == nil {
|
||||
goto success
|
||||
}
|
||||
errYaml = yaml.Unmarshal(b, &instrument)
|
||||
if errYaml == nil {
|
||||
goto success
|
||||
}
|
||||
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||
if err4kp == nil {
|
||||
song := t.Song()
|
||||
song.Score = t.Song().Score.Copy()
|
||||
song.Patch = patch
|
||||
t.SetSong(song)
|
||||
return true
|
||||
}
|
||||
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||
if err4ki == nil {
|
||||
goto success
|
||||
}
|
||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error, time.Second*3)
|
||||
return false
|
||||
success:
|
||||
if f, ok := r.(*os.File); ok {
|
||||
filename := f.Name()
|
||||
// the 4klang instrument names are junk, replace them with the filename without extension
|
||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||
}
|
||||
if len(instrument.Units) == 0 {
|
||||
t.Alert.Update("The instrument file is malformed", Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
t.SetInstrument(instrument)
|
||||
if t.Instrument().Comment != "" {
|
||||
t.InstrumentEditor.ExpandComment()
|
||||
}
|
||||
return true
|
||||
}
|
||||
536
tracker/gioui/instrument_editor.go
Normal file
536
tracker/gioui/instrument_editor.go
Normal file
@ -0,0 +1,536 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *ActionClickable
|
||||
enlargeBtn *BoolClickable
|
||||
deleteInstrumentBtn *ActionClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
addUnitBtn *ActionClickable
|
||||
presetMenuBtn *TipClickable
|
||||
commentExpandBtn *BoolClickable
|
||||
commentEditor *widget.Editor
|
||||
commentString tracker.String
|
||||
nameEditor *widget.Editor
|
||||
nameString tracker.String
|
||||
searchEditor *widget.Editor
|
||||
instrumentDragList *DragList
|
||||
unitDragList *DragList
|
||||
presetDragList *DragList
|
||||
unitEditor *UnitEditor
|
||||
tag bool
|
||||
wasFocused bool
|
||||
presetMenuItems []MenuItem
|
||||
presetMenu Menu
|
||||
commentKeyFilters []event.Filter
|
||||
searchkeyFilters []event.Filter
|
||||
nameKeyFilters []event.Filter
|
||||
}
|
||||
|
||||
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||
ret := &InstrumentEditor{
|
||||
newInstrumentBtn: NewActionClickable(model.AddInstrument()),
|
||||
enlargeBtn: NewBoolClickable(model.InstrEnlarged().Bool()),
|
||||
deleteInstrumentBtn: NewActionClickable(model.DeleteInstrument()),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
addUnitBtn: NewActionClickable(model.AddUnit(false)),
|
||||
commentExpandBtn: NewBoolClickable(model.CommentExpanded().Bool()),
|
||||
presetMenuBtn: new(TipClickable),
|
||||
commentEditor: new(widget.Editor),
|
||||
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
searchEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
|
||||
commentString: model.InstrumentComment().String(),
|
||||
nameString: model.InstrumentName().String(),
|
||||
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
|
||||
unitDragList: NewDragList(model.Units().List(), layout.Vertical),
|
||||
unitEditor: NewUnitEditor(model),
|
||||
presetMenuItems: []MenuItem{},
|
||||
}
|
||||
model.IterateInstrumentPresets(func(index int, name string) bool {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)})
|
||||
return true
|
||||
})
|
||||
for k := range noteMap {
|
||||
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: k, Focus: ret.commentEditor})
|
||||
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: k, Focus: ret.searchEditor})
|
||||
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: k, Focus: ret.nameEditor})
|
||||
}
|
||||
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.commentEditor})
|
||||
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.searchEditor})
|
||||
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.nameEditor})
|
||||
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.commentEditor})
|
||||
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.searchEditor})
|
||||
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.nameEditor})
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focus() {
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focused() bool {
|
||||
return ie.unitDragList.focused
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) childFocused(gtx C) bool {
|
||||
return ie.unitEditor.sliderList.Focused() ||
|
||||
ie.instrumentDragList.Focused() || gtx.Source.Focused(ie.commentEditor) || gtx.Source.Focused(ie.nameEditor) || gtx.Source.Focused(ie.searchEditor) ||
|
||||
gtx.Source.Focused(ie.addUnitBtn.Clickable) || gtx.Source.Focused(ie.commentExpandBtn.Clickable) || gtx.Source.Focused(ie.presetMenuBtn.Clickable) ||
|
||||
gtx.Source.Focused(ie.deleteInstrumentBtn.Clickable) || gtx.Source.Focused(ie.copyInstrumentBtn.Clickable)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
ie.wasFocused = ie.Focused() || ie.childFocused(gtx)
|
||||
fullscreenBtnStyle := ToggleIcon(gtx, t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, "Enlarge (Ctrl+E)", "Shrink (Ctrl+E)")
|
||||
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, "Octave down (<) or up (>)")
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
return dims
|
||||
}
|
||||
|
||||
newBtnStyle := ActionIcon(gtx, t.Theme, ie.newInstrumentBtn, icons.ContentAdd, "Add\ninstrument\n(Ctrl+I)")
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{}.Layout(
|
||||
gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.layoutInstrumentList(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutInstrumentHeader(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutUnitList(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.unitEditor.Layout(gtx, t)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
header := func(gtx C) D {
|
||||
commentExpandBtnStyle := ToggleIcon(gtx, t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, "Expand comment", "Collapse comment")
|
||||
presetMenuBtnStyle := TipIcon(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, "Load preset")
|
||||
copyInstrumentBtnStyle := TipIcon(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
||||
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||
deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, "Delete\ninstrument")
|
||||
|
||||
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)
|
||||
|
||||
for ie.copyInstrumentBtn.Clickable.Clicked(gtx) {
|
||||
if contents, ok := t.Instruments().List().CopyElements(); ok {
|
||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
||||
t.Alerts().Add("Instrument copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
|
||||
for ie.saveInstrumentBtn.Clickable.Clicked(gtx) {
|
||||
writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.SaveInstrument(writer)
|
||||
}
|
||||
|
||||
for ie.loadInstrumentBtn.Clickable.Clicked(gtx) {
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.LoadInstrument(reader)
|
||||
}
|
||||
|
||||
header := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white, t.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, "Number of voices for this instrument")
|
||||
dims := numStyle.Layout(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
dims := presetMenuBtnStyle.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
|
||||
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
|
||||
m.Layout(gtx, ie.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(saveInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(loadInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(copyInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
|
||||
for ie.presetMenuBtn.Clickable.Clicked(gtx) {
|
||||
ie.presetMenu.Visible = true
|
||||
}
|
||||
|
||||
if ie.commentExpandBtn.Bool.Value() || gtx.Source.Focused(ie.commentEditor) { // we draw once the widget after it manages to lose focus
|
||||
if ie.commentEditor.Text() != ie.commentString.Value() {
|
||||
ie.commentEditor.SetText(ie.commentString.Value())
|
||||
}
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(header),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
for {
|
||||
event, ok := gtx.Event(ie.commentKeyFilters...)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
editorStyle := material.Editor(t.Theme, ie.commentEditor, "Comment")
|
||||
editorStyle.Color = highEmphasisTextColor
|
||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, editorStyle.Layout)
|
||||
}),
|
||||
)
|
||||
ie.commentString.Set(ie.commentEditor.Text())
|
||||
return ret
|
||||
}
|
||||
return header(gtx)
|
||||
}
|
||||
|
||||
return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.Theme.Shaper}
|
||||
if i == ie.instrumentDragList.TrackerList.Selected() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
name, level, ok := (*tracker.Instruments)(t.Model).Item(i)
|
||||
if !ok {
|
||||
labelStyle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
k := byte(255 - level*127)
|
||||
color := color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
||||
if i == ie.instrumentDragList.TrackerList.Selected() {
|
||||
if n := name; n != ie.nameEditor.Text() {
|
||||
ie.nameEditor.SetText(n)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.nameEditor, "Instr")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
for {
|
||||
ev, ok := ie.nameEditor.Update(gtx)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, ok = ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.instrumentDragList.Focus()
|
||||
continue
|
||||
}
|
||||
}
|
||||
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()
|
||||
return editor.Layout(gtx)
|
||||
})
|
||||
for { // don't let key presses flow through from the editor
|
||||
event, ok := gtx.Event(ie.nameKeyFilters...)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
ie.nameString.Set(ie.nameEditor.Text())
|
||||
return dims
|
||||
}
|
||||
if name == "" {
|
||||
name = "Instr"
|
||||
}
|
||||
labelStyle := LabelStyle{Text: name, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(grabhandle.Layout),
|
||||
layout.Rigid(label),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
color := inactiveLightSurfaceColor
|
||||
if ie.wasFocused {
|
||||
color = activeLightSurfaceColor
|
||||
}
|
||||
instrumentList := FilledDragList(t.Theme, ie.instrumentDragList, element, nil)
|
||||
instrumentList.SelectedColor = color
|
||||
instrumentList.HoverColor = instrumentHoverColor
|
||||
instrumentList.ScrollBarWidth = unit.Dp(6)
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.Filter{Focus: ie.instrumentDragList, Name: key.NameDownArrow},
|
||||
key.Filter{Focus: ie.instrumentDragList, Name: key.NameReturn},
|
||||
key.Filter{Focus: ie.instrumentDragList, Name: key.NameEnter},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
ie.unitDragList.Focus()
|
||||
case key.NameReturn, key.NameEnter:
|
||||
gtx.Execute(key.FocusCmd{Tag: ie.nameEditor})
|
||||
l := len(ie.nameEditor.Text())
|
||||
ie.nameEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dims := instrumentList.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
instrumentList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
|
||||
// TODO: how to ie.unitDragList.Focus()
|
||||
addUnitBtnStyle := ActionIcon(gtx, t.Theme, ie.addUnitBtn, icons.ContentAdd, "Add unit (Enter)")
|
||||
addUnitBtnStyle.IconButtonStyle.Color = t.Theme.ContrastFg
|
||||
addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
|
||||
|
||||
index := 0
|
||||
var units [256]tracker.UnitListItem
|
||||
(*tracker.Units)(t.Model).Iterate(func(item tracker.UnitListItem) (ok bool) {
|
||||
units[index] = item
|
||||
index++
|
||||
return index <= 256
|
||||
})
|
||||
count := intMin(ie.unitDragList.TrackerList.Count(), 256)
|
||||
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Dp(unit.Dp(20))))
|
||||
if i < 0 || i >= count {
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}
|
||||
u := units[i]
|
||||
var color color.NRGBA = white
|
||||
f := labelDefaultFont
|
||||
|
||||
var stackText string
|
||||
stackText = strconv.FormatInt(int64(u.StackAfter), 10)
|
||||
if u.StackNeed > u.StackBefore {
|
||||
color = errorColor
|
||||
(*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error)
|
||||
} else if i == count-1 && u.StackAfter != 0 {
|
||||
color = warningColor
|
||||
(*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning)
|
||||
}
|
||||
if u.Disabled {
|
||||
color = disabledTextColor
|
||||
f.Style = font.Italic
|
||||
}
|
||||
|
||||
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
rightMargin := layout.Inset{Right: unit.Dp(10)}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
if i == ie.unitDragList.TrackerList.Selected() {
|
||||
editor := material.Editor(t.Theme, ie.searchEditor, "---")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = f
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
txt := u.Type
|
||||
str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)}
|
||||
if t.UnitSearching().Value() {
|
||||
txt = str.Value()
|
||||
}
|
||||
if ie.searchEditor.Text() != txt {
|
||||
ie.searchEditor.SetText(txt)
|
||||
}
|
||||
for {
|
||||
ev, ok := ie.searchEditor.Update(gtx)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, ok = ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
txt := ""
|
||||
ie.unitDragList.Focus()
|
||||
if text := ie.searchEditor.Text(); text != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, ie.searchEditor.Text()) {
|
||||
txt = n
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Units().SetSelectedType(txt)
|
||||
t.UnitSearching().Bool().Set(false)
|
||||
continue
|
||||
}
|
||||
}
|
||||
ret := editor.Layout(gtx)
|
||||
for { // don't let key presses flow through from the editor
|
||||
event, ok := gtx.Event(ie.searchkeyFilters...)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
if ie.searchEditor.Text() != txt {
|
||||
str.Set(ie.searchEditor.Text())
|
||||
}
|
||||
return ret
|
||||
} else {
|
||||
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: f, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
if unitNameLabel.Text == "" {
|
||||
unitNameLabel.Text = "---"
|
||||
}
|
||||
return unitNameLabel.Layout(gtx)
|
||||
}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return rightMargin.Layout(gtx, stackLabel.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ie.unitDragList, element, nil)
|
||||
return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.Filter{Focus: ie.unitDragList, Name: key.NameRightArrow},
|
||||
key.Filter{Focus: ie.unitDragList, Name: key.NameEnter, Optional: key.ModCtrl},
|
||||
key.Filter{Focus: ie.unitDragList, Name: key.NameReturn, Optional: key.ModCtrl},
|
||||
key.Filter{Focus: ie.unitDragList, Name: key.NameDeleteBackward},
|
||||
key.Filter{Focus: ie.unitDragList, Name: key.NameEscape},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
ie.instrumentDragList.Focus()
|
||||
case key.NameRightArrow:
|
||||
ie.unitEditor.sliderList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.Units().SetSelectedType("")
|
||||
gtx.Execute(key.FocusCmd{Tag: ie.searchEditor})
|
||||
l := len(ie.searchEditor.Text())
|
||||
ie.searchEditor.SetCaret(l, l)
|
||||
case key.NameEnter, key.NameReturn:
|
||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
ie.searchEditor.SetText("")
|
||||
gtx.Execute(key.FocusCmd{Tag: ie.searchEditor})
|
||||
l := len(ie.searchEditor.Text())
|
||||
ie.searchEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Constraints.Max.Y))
|
||||
dims := unitList.Layout(gtx)
|
||||
unitList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
return margin.Layout(gtx, addUnitBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func clamp(i, min, max int) int {
|
||||
if i < min {
|
||||
return min
|
||||
}
|
||||
if i > max {
|
||||
return max
|
||||
}
|
||||
return i
|
||||
}
|
||||
@ -1,578 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/eventx"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *TipClickable
|
||||
enlargeBtn *TipClickable
|
||||
deleteInstrumentBtn *TipClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
addUnitBtn *TipClickable
|
||||
commentExpandBtn *TipClickable
|
||||
presetMenuBtn *TipClickable
|
||||
commentEditor *widget.Editor
|
||||
nameEditor *widget.Editor
|
||||
unitTypeEditor *widget.Editor
|
||||
instrumentDragList *DragList
|
||||
instrumentScrollBar *ScrollBar
|
||||
unitDragList *DragList
|
||||
unitScrollBar *ScrollBar
|
||||
confirmInstrDelete *Dialog
|
||||
paramEditor *ParamEditor
|
||||
stackUse []int
|
||||
tag bool
|
||||
wasFocused bool
|
||||
commentExpanded bool
|
||||
voiceLevels [vm.MAX_VOICES]float32
|
||||
presetMenuItems []MenuItem
|
||||
presetMenu Menu
|
||||
}
|
||||
|
||||
func NewInstrumentEditor() *InstrumentEditor {
|
||||
ret := &InstrumentEditor{
|
||||
newInstrumentBtn: new(TipClickable),
|
||||
enlargeBtn: new(TipClickable),
|
||||
deleteInstrumentBtn: new(TipClickable),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
addUnitBtn: new(TipClickable),
|
||||
commentExpandBtn: new(TipClickable),
|
||||
presetMenuBtn: new(TipClickable),
|
||||
commentEditor: new(widget.Editor),
|
||||
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
unitTypeEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
|
||||
instrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1},
|
||||
instrumentScrollBar: &ScrollBar{Axis: layout.Horizontal},
|
||||
unitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1},
|
||||
unitScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
confirmInstrDelete: new(Dialog),
|
||||
paramEditor: NewParamEditor(),
|
||||
presetMenuItems: []MenuItem{},
|
||||
}
|
||||
for _, instr := range tracker.InstrumentPresets {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: instr.Name, IconBytes: icons.ImageAudiotrack})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *InstrumentEditor) ExpandComment() {
|
||||
t.commentExpanded = true
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focus() {
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focused() bool {
|
||||
return ie.unitDragList.focused
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) ChildFocused() bool {
|
||||
return ie.paramEditor.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() || ie.unitTypeEditor.Focused() ||
|
||||
ie.addUnitBtn.Clickable.Focused() || ie.commentExpandBtn.Clickable.Focused() || ie.presetMenuBtn.Clickable.Focused() || ie.deleteInstrumentBtn.Clickable.Focused() || ie.copyInstrumentBtn.Clickable.Focused()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
ie.wasFocused = ie.Focused() || ie.ChildFocused()
|
||||
for _, e := range gtx.Events(&ie.tag) {
|
||||
switch e.(type) {
|
||||
case pointer.Event:
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &ie.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
|
||||
enlargeTip := "Enlarge"
|
||||
icon := icons.NavigationFullscreen
|
||||
if t.InstrEnlarged() {
|
||||
icon = icons.NavigationFullscreenExit
|
||||
enlargeTip = "Shrink"
|
||||
}
|
||||
|
||||
fullscreenBtnStyle := IconButton(t.Theme, ie.enlargeBtn, icon, true, enlargeTip)
|
||||
for ie.enlargeBtn.Clickable.Clicked() {
|
||||
t.SetInstrEnlarged(!t.InstrEnlarged())
|
||||
}
|
||||
for ie.newInstrumentBtn.Clickable.Clicked() {
|
||||
t.AddInstrument(true)
|
||||
}
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
t.OctaveNumberInput.Value = t.Octave()
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, 0, 9, "Octave down (<) or up (>)")
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetOctave(t.OctaveNumberInput.Value)
|
||||
return dims
|
||||
}
|
||||
newBtnStyle := IconButton(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, t.CanAddInstrument(), "Add\ninstrument")
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{}.Layout(
|
||||
gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return ie.layoutInstrumentNames(gtx, t)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return ie.instrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.Song().Patch), &ie.instrumentDragList.List.Position)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white, t.TextShaper)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutInstrumentHeader(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.layoutInstrumentEditor(gtx, t)
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
header := func(gtx C) D {
|
||||
collapseIcon := icons.NavigationExpandLess
|
||||
commentTip := "Collapse comment"
|
||||
if !ie.commentExpanded {
|
||||
collapseIcon = icons.NavigationExpandMore
|
||||
commentTip = "Expand comment"
|
||||
}
|
||||
|
||||
commentExpandBtnStyle := IconButton(t.Theme, ie.commentExpandBtn, collapseIcon, true, commentTip)
|
||||
presetMenuBtnStyle := IconButton(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, true, "Load preset")
|
||||
copyInstrumentBtnStyle := IconButton(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, true, "Copy instrument")
|
||||
saveInstrumentBtnStyle := IconButton(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, true, "Save instrument")
|
||||
loadInstrumentBtnStyle := IconButton(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, true, "Load instrument")
|
||||
deleteInstrumentBtnStyle := IconButton(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, t.CanDeleteInstrument(), "Delete\ninstrument")
|
||||
|
||||
m := t.PopupMenu(&ie.presetMenu)
|
||||
|
||||
for item, clicked := ie.presetMenu.Clicked(); clicked; item, clicked = ie.presetMenu.Clicked() {
|
||||
t.SetInstrument(tracker.InstrumentPresets[item])
|
||||
}
|
||||
|
||||
header := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white, t.TextShaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
maxRemain := t.MaxInstrumentVoices()
|
||||
t.InstrumentVoices.Value = t.Instrument().NumVoices
|
||||
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, 0, maxRemain, "Number of voices for this instrument")
|
||||
dims := numStyle.Layout(gtx)
|
||||
t.SetInstrumentVoices(t.InstrumentVoices.Value)
|
||||
return dims
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
dims := presetMenuBtnStyle.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
|
||||
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
|
||||
m.Layout(gtx, ie.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(saveInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(loadInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(copyInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
|
||||
for ie.presetMenuBtn.Clickable.Clicked() {
|
||||
ie.presetMenu.Visible = true
|
||||
}
|
||||
|
||||
for ie.commentExpandBtn.Clickable.Clicked() {
|
||||
ie.commentExpanded = !ie.commentExpanded
|
||||
if !ie.commentExpanded {
|
||||
key.FocusOp{Tag: &ie.tag}.Add(gtx.Ops) // clear focus
|
||||
}
|
||||
}
|
||||
if ie.commentExpanded || ie.commentEditor.Focused() { // we draw once the widget after it manages to lose focus
|
||||
if ie.commentEditor.Text() != t.Instrument().Comment {
|
||||
ie.commentEditor.SetText(t.Instrument().Comment)
|
||||
}
|
||||
editorStyle := material.Editor(t.Theme, ie.commentEditor, "Comment")
|
||||
editorStyle.Color = highEmphasisTextColor
|
||||
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(header),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
spy, spiedGtx := eventx.Enspy(gtx)
|
||||
ret := layout.UniformInset(unit.Dp(6)).Layout(spiedGtx, editorStyle.Layout)
|
||||
for _, group := range spy.AllEvents() {
|
||||
for _, event := range group.Items {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
if e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}),
|
||||
)
|
||||
t.SetInstrumentComment(ie.commentEditor.Text())
|
||||
return ret
|
||||
}
|
||||
return header(gtx)
|
||||
}
|
||||
|
||||
for ie.copyInstrumentBtn.Clickable.Clicked() {
|
||||
contents, err := yaml.Marshal(t.Instrument())
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
for ie.deleteInstrumentBtn.Clickable.Clicked() {
|
||||
if t.CanDeleteInstrument() {
|
||||
dialogStyle := ConfirmDialog(t.Theme, ie.confirmInstrDelete, "Are you sure you want to delete this instrument?", t.TextShaper)
|
||||
ie.confirmInstrDelete.Visible = true
|
||||
t.ModalDialog = dialogStyle.Layout
|
||||
}
|
||||
}
|
||||
for ie.confirmInstrDelete.BtnOk.Clicked() {
|
||||
t.DeleteInstrument(false)
|
||||
t.ModalDialog = nil
|
||||
}
|
||||
for ie.confirmInstrDelete.BtnCancel.Clicked() {
|
||||
t.ModalDialog = nil
|
||||
}
|
||||
for ie.saveInstrumentBtn.Clickable.Clicked() {
|
||||
t.SaveInstrument()
|
||||
}
|
||||
|
||||
for ie.loadInstrumentBtn.Clickable.Clicked() {
|
||||
t.LoadInstrument()
|
||||
}
|
||||
return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentNames(gtx C, t *Tracker) D {
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.TextShaper}
|
||||
if i == t.InstrIndex() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
c := float32(0.0)
|
||||
voice := t.Song().Patch.FirstVoiceForInstrument(i)
|
||||
loopMax := t.Song().Patch[i].NumVoices
|
||||
if loopMax > vm.MAX_VOICES {
|
||||
loopMax = vm.MAX_VOICES
|
||||
}
|
||||
for j := 0; j < loopMax; j++ {
|
||||
vc := ie.voiceLevels[voice]
|
||||
if c < vc {
|
||||
c = vc
|
||||
}
|
||||
voice++
|
||||
}
|
||||
k := byte(255 - c*127)
|
||||
color := color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
||||
if i == t.InstrIndex() {
|
||||
for _, ev := range ie.nameEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.instrumentDragList.Focus()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if n := t.Instrument().Name; n != ie.nameEditor.Text() {
|
||||
ie.nameEditor.SetText(n)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.nameEditor, "Instr")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
dims := layout.Center.Layout(gtx, editor.Layout)
|
||||
t.SetInstrumentName(ie.nameEditor.Text())
|
||||
return dims
|
||||
}
|
||||
text := t.Song().Patch[i].Name
|
||||
if text == "" {
|
||||
text = "Instr"
|
||||
}
|
||||
labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: color, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(grabhandle.Layout),
|
||||
layout.Rigid(label),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
color := inactiveLightSurfaceColor
|
||||
if ie.wasFocused {
|
||||
color = activeLightSurfaceColor
|
||||
}
|
||||
instrumentList := FilledDragList(t.Theme, ie.instrumentDragList, len(t.Song().Patch), element, t.SwapInstruments)
|
||||
instrumentList.SelectedColor = color
|
||||
instrumentList.HoverColor = instrumentHoverColor
|
||||
|
||||
ie.instrumentDragList.SelectedItem = t.InstrIndex()
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.instrumentDragList, Keys: "↓|⏎|⌤"}.Add(gtx.Ops)
|
||||
|
||||
for _, event := range gtx.Events(ie.instrumentDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
ie.unitDragList.Focus()
|
||||
case key.NameReturn, key.NameEnter:
|
||||
ie.nameEditor.Focus()
|
||||
l := len(ie.nameEditor.Text())
|
||||
ie.nameEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dims := instrumentList.Layout(gtx)
|
||||
|
||||
if t.InstrIndex() != ie.instrumentDragList.SelectedItem {
|
||||
t.SetInstrIndex(ie.instrumentDragList.SelectedItem)
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
return dims
|
||||
}
|
||||
func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D {
|
||||
for ie.addUnitBtn.Clickable.Clicked() {
|
||||
t.AddUnit(true)
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
addUnitBtnStyle := IconButton(t.Theme, ie.addUnitBtn, icons.ContentAdd, true, "Add unit (Ctrl+Enter)")
|
||||
addUnitBtnStyle.IconButtonStyle.Color = t.Theme.ContrastFg
|
||||
addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
|
||||
|
||||
units := t.Instrument().Units
|
||||
for len(ie.stackUse) < len(units) {
|
||||
ie.stackUse = append(ie.stackUse, 0)
|
||||
}
|
||||
|
||||
stackHeight := 0
|
||||
for i, u := range units {
|
||||
stackHeight += u.StackChange()
|
||||
ie.stackUse[i] = stackHeight
|
||||
}
|
||||
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Dp(unit.Dp(20))))
|
||||
u := units[i]
|
||||
var color color.NRGBA = white
|
||||
|
||||
var stackText string
|
||||
if i < len(ie.stackUse) {
|
||||
stackText = strconv.FormatInt(int64(ie.stackUse[i]), 10)
|
||||
var prevStackUse int
|
||||
if i > 0 {
|
||||
prevStackUse = ie.stackUse[i-1]
|
||||
}
|
||||
if stackNeed := u.StackNeed(); stackNeed > prevStackUse {
|
||||
color = errorColor
|
||||
typeString := u.Type
|
||||
if u.Parameters["stereo"] == 1 {
|
||||
typeString += " (stereo)"
|
||||
}
|
||||
t.Alert.Update(fmt.Sprintf("%v needs at least %v input signals, got %v", typeString, stackNeed, prevStackUse), Error, 0)
|
||||
} else if i == len(units)-1 && ie.stackUse[i] != 0 {
|
||||
color = warningColor
|
||||
t.Alert.Update(fmt.Sprintf("Instrument leaves %v signal(s) on the stack", ie.stackUse[i]), Warning, 0)
|
||||
}
|
||||
}
|
||||
|
||||
var unitName layout.Widget
|
||||
if i == t.UnitIndex() {
|
||||
for _, ev := range ie.unitTypeEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.unitDragList.Focus()
|
||||
if text := ie.unitTypeEditor.Text(); text != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, ie.unitTypeEditor.Text()) {
|
||||
t.SetUnitType(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.SetUnitType("")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !ie.unitTypeEditor.Focused() && !ie.paramEditor.Focused() && ie.unitTypeEditor.Text() != t.Unit().Type {
|
||||
ie.unitTypeEditor.SetText(t.Unit().Type)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.unitTypeEditor, "---")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
unitName = editor.Layout
|
||||
} else {
|
||||
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
if unitNameLabel.Text == "" {
|
||||
unitNameLabel.Text = "---"
|
||||
}
|
||||
unitName = unitNameLabel.Layout
|
||||
}
|
||||
|
||||
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
rightMargin := layout.Inset{Right: unit.Dp(10)}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Flexed(1, unitName),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return rightMargin.Layout(gtx, stackLabel.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ie.unitDragList, len(units), element, t.SwapUnits)
|
||||
return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.unitDragList, Keys: "→|⏎|⌫|⌦|⎋|Ctrl-⏎|Ctrl-C|Ctrl-X"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(ie.unitDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
ie.instrumentDragList.Focus()
|
||||
case key.NameRightArrow:
|
||||
ie.paramEditor.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.SetUnitType("")
|
||||
ie.unitTypeEditor.Focus()
|
||||
l := len(ie.unitTypeEditor.Text())
|
||||
ie.unitTypeEditor.SetCaret(l, l)
|
||||
case key.NameDeleteForward:
|
||||
t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
|
||||
ie.unitDragList.SelectedItem2 = t.UnitIndex()
|
||||
case "X":
|
||||
units := t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
|
||||
ie.unitDragList.SelectedItem2 = t.UnitIndex()
|
||||
contents, err := yaml.Marshal(units)
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit(s) cut to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case "C":
|
||||
a := clamp(ie.unitDragList.SelectedItem, 0, len(t.Instrument().Units)-1)
|
||||
b := clamp(ie.unitDragList.SelectedItem2, 0, len(t.Instrument().Units)-1)
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
units := t.Instrument().Units[a : b+1]
|
||||
contents, err := yaml.Marshal(units)
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit(s) copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case key.NameReturn:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AddUnit(true)
|
||||
ie.unitDragList.SelectedItem2 = ie.unitDragList.SelectedItem
|
||||
ie.unitTypeEditor.SetText("")
|
||||
}
|
||||
ie.unitTypeEditor.Focus()
|
||||
l := len(ie.unitTypeEditor.Text())
|
||||
ie.unitTypeEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ie.unitDragList.SelectedItem = t.UnitIndex()
|
||||
dims := unitList.Layout(gtx)
|
||||
if t.UnitIndex() != ie.unitDragList.SelectedItem {
|
||||
t.SetUnitIndex(ie.unitDragList.SelectedItem)
|
||||
ie.unitTypeEditor.SetText(t.Unit().Type)
|
||||
}
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
return margin.Layout(gtx, addUnitBtnStyle.Layout)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return ie.unitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &ie.unitDragList.List.Position)
|
||||
}))
|
||||
}),
|
||||
layout.Rigid(ie.paramEditor.Bind(t)))
|
||||
})
|
||||
}
|
||||
|
||||
func clamp(i, min, max int) int {
|
||||
if i < min {
|
||||
return min
|
||||
}
|
||||
if i > max {
|
||||
return max
|
||||
}
|
||||
return i
|
||||
}
|
||||
@ -1,16 +1,11 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/op"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var noteMap = map[string]int{
|
||||
var noteMap = map[key.Name]int{
|
||||
"Z": -12,
|
||||
"S": -11,
|
||||
"X": -10,
|
||||
@ -46,116 +41,138 @@ var noteMap = map[string]int{
|
||||
}
|
||||
|
||||
// KeyEvent handles incoming key events and returns true if repaint is needed.
|
||||
func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
if e.State == key.Press {
|
||||
switch e.Name {
|
||||
case "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, err := yaml.Marshal(t.Song())
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(o)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
clipboard.ReadOp{Tag: t}.Add(o)
|
||||
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
||||
return
|
||||
}
|
||||
case "Z":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Undo()
|
||||
t.Model.Undo().Do()
|
||||
return
|
||||
}
|
||||
case "Y":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Redo()
|
||||
t.Model.Redo().Do()
|
||||
return
|
||||
}
|
||||
case "D":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.UnitDisabled().Bool().Toggle()
|
||||
return
|
||||
}
|
||||
case "L":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.LoopToggle().Bool().Toggle()
|
||||
return
|
||||
}
|
||||
case "N":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.NewSong(false)
|
||||
t.NewSong().Do()
|
||||
return
|
||||
}
|
||||
case "S":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SaveSongFile()
|
||||
t.SaveSong().Do()
|
||||
return
|
||||
}
|
||||
case "O":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.OpenSongFile(false)
|
||||
t.OpenSong().Do()
|
||||
return
|
||||
}
|
||||
case "I":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.DeleteInstrument().Do()
|
||||
} else {
|
||||
t.AddInstrument().Do()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "T":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.DeleteTrack().Do()
|
||||
} else {
|
||||
t.AddTrack().Do()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "E":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.InstrEnlarged().Bool().Toggle()
|
||||
return
|
||||
}
|
||||
case "W":
|
||||
if e.Modifiers.Contain(key.ModShortcut) && canQuit {
|
||||
t.Quit().Do()
|
||||
return
|
||||
}
|
||||
case "F1":
|
||||
t.OrderEditor.Focus()
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
return
|
||||
case "F2":
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
return
|
||||
case "F3":
|
||||
t.InstrumentEditor.Focus()
|
||||
return
|
||||
case "F4":
|
||||
t.TrackEditor.Focus()
|
||||
return
|
||||
case "F5":
|
||||
t.SetNoteTracking(true)
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
t.SongPanel.RewindBtn.Action.Do()
|
||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
||||
return
|
||||
case "F6":
|
||||
t.SetNoteTracking(false)
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
case "F6", "Space":
|
||||
t.SongPanel.PlayingBtn.Bool.Toggle()
|
||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
||||
return
|
||||
case "F7":
|
||||
t.SongPanel.RecordBtn.Bool.Toggle()
|
||||
return
|
||||
case "F8":
|
||||
t.SetPlaying(false)
|
||||
t.SongPanel.NoteTracking.Bool.Toggle()
|
||||
return
|
||||
case "F12":
|
||||
t.Panic().Bool().Toggle()
|
||||
return
|
||||
case "Space":
|
||||
if !t.Playing() && !t.InstrEnlarged() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case `\`, `<`, `>`:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetOctave(t.Octave() + 1)
|
||||
t.OctaveNumberInput.Int.Add(1)
|
||||
} else {
|
||||
t.SetOctave(t.Octave() - 1)
|
||||
t.OctaveNumberInput.Int.Add(-1)
|
||||
}
|
||||
case key.NameTab:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
switch {
|
||||
case t.OrderEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
case t.TrackEditor.Focused():
|
||||
t.OrderEditor.Focus()
|
||||
case t.OrderEditor.scrollTable.Focused():
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
case t.TrackEditor.scrollTable.Focused():
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
if t.InstrEnlarged() {
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
} else {
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
}
|
||||
default:
|
||||
t.InstrumentEditor.Focus()
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case t.OrderEditor.Focused():
|
||||
t.TrackEditor.Focus()
|
||||
case t.TrackEditor.Focused():
|
||||
case t.OrderEditor.scrollTable.Focused():
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
case t.TrackEditor.scrollTable.Focused():
|
||||
t.InstrumentEditor.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
default:
|
||||
if t.InstrEnlarged() {
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.InstrumentEditor.Focus()
|
||||
} else {
|
||||
t.OrderEditor.Focus()
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,28 +183,12 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
}
|
||||
}
|
||||
|
||||
// NumberPressed handles incoming presses while in either of the hex number columns
|
||||
func (t *Tracker) NumberPressed(iv byte) {
|
||||
val := t.Note()
|
||||
if val == 1 {
|
||||
val = 0
|
||||
}
|
||||
if t.LowNibble() {
|
||||
val = (val & 0xF0) | (iv & 0xF)
|
||||
} else {
|
||||
val = ((iv & 0xF) << 4) | (val & 0xF)
|
||||
}
|
||||
t.SetNote(val)
|
||||
}
|
||||
|
||||
func (t *Tracker) JammingPressed(e key.Event) byte {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := noteAsValue(t.OctaveNumberInput.Value, val)
|
||||
instr := t.InstrIndex()
|
||||
noteID := tracker.NoteIDInstr(instr, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
|
||||
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
@ -196,7 +197,7 @@ func (t *Tracker) JammingPressed(e key.Event) byte {
|
||||
|
||||
func (t *Tracker) JammingReleased(e key.Event) bool {
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
noteID.NoteOff()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -24,28 +24,25 @@ type LabelStyle struct {
|
||||
}
|
||||
|
||||
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Stack{Alignment: l.Alignment}.Layout(gtx,
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops)
|
||||
op.Offset(image.Pt(2, 2)).Add(gtx.Ops)
|
||||
dims := widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
return layout.Dimensions{
|
||||
Size: dims.Size.Add(image.Pt(2, 2)),
|
||||
Baseline: dims.Baseline,
|
||||
}
|
||||
}),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
return widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
}),
|
||||
)
|
||||
return l.Alignment.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Point{}
|
||||
paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops)
|
||||
offs := op.Offset(image.Pt(2, 2)).Push(gtx.Ops)
|
||||
widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
offs.Pop()
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
dims := widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
return layout.Dimensions{
|
||||
Size: dims.Size,
|
||||
Baseline: dims.Baseline,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||
// this is the top level input handler for the whole app
|
||||
// it handles all the global key events and clipboard events
|
||||
// we need to tell gio that we handle tabs too; otherwise
|
||||
// it will steal them for focus switching
|
||||
key.InputOp{Tag: t, Keys: "Tab|Shift-Tab"}.Add(gtx.Ops)
|
||||
for _, ev := range gtx.Events(t) {
|
||||
switch e := ev.(type) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx.Ops)
|
||||
case clipboard.Event:
|
||||
t.UnmarshalContent([]byte(e.Text))
|
||||
}
|
||||
}
|
||||
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
if t.InstrEnlarged() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.Alert.Layout(gtx)
|
||||
dstyle := ConfirmDialog(t.Theme, t.ConfirmSongDialog, "Do you want to save your changes to the song? Your changes will be lost if you don't save them.", t.TextShaper)
|
||||
dstyle.ShowAlt = true
|
||||
dstyle.OkStyle.Text = "Save"
|
||||
dstyle.AltStyle.Text = "Don't save"
|
||||
dstyle.Layout(gtx)
|
||||
for t.ConfirmSongDialog.BtnOk.Clicked() {
|
||||
if t.SaveSongFile() {
|
||||
t.confirmedSongAction()
|
||||
}
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
for t.ConfirmSongDialog.BtnAlt.Clicked() {
|
||||
t.confirmedSongAction()
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
for t.ConfirmSongDialog.BtnCancel.Clicked() {
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
dstyle = ConfirmDialog(t.Theme, t.WaveTypeDialog, "Export .wav in int16 or float32 sample format?", t.TextShaper)
|
||||
dstyle.ShowAlt = true
|
||||
dstyle.OkStyle.Text = "Int16"
|
||||
dstyle.AltStyle.Text = "Float32"
|
||||
dstyle.Layout(gtx)
|
||||
for t.WaveTypeDialog.BtnOk.Clicked() {
|
||||
t.ExportWav(true)
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
for t.WaveTypeDialog.BtnAlt.Clicked() {
|
||||
t.ExportWav(false)
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
for t.WaveTypeDialog.BtnCancel.Clicked() {
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
if t.ModalDialog != nil {
|
||||
t.ModalDialog(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) confirmedSongAction() {
|
||||
switch t.ConfirmSongActionType {
|
||||
case ConfirmLoad:
|
||||
t.OpenSongFile(true)
|
||||
case ConfirmNew:
|
||||
t.NewSong(true)
|
||||
case ConfirmQuit:
|
||||
t.Quit(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) NewSong(forced bool) {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmNew
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return
|
||||
}
|
||||
t.ResetSong()
|
||||
t.SetFilePath("")
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.OrderEditor.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.TrackEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
return t.TopHorizontalSplit.Layout(gtx,
|
||||
t.layoutSongPanel,
|
||||
func(gtx C) D {
|
||||
return t.InstrumentEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@ -12,6 +13,8 @@ import (
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type Menu struct {
|
||||
@ -40,7 +43,7 @@ type MenuItem struct {
|
||||
IconBytes []byte
|
||||
Text string
|
||||
ShortcutText string
|
||||
Disabled bool
|
||||
Doer tracker.Action
|
||||
}
|
||||
|
||||
func (m *Menu) Clicked() (int, bool) {
|
||||
@ -57,20 +60,27 @@ func (m *Menu) Clicked() (int, bool) {
|
||||
|
||||
func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
contents := func(gtx C) D {
|
||||
for i := range items {
|
||||
for i, item := range items {
|
||||
// make sure we have a tag for every item
|
||||
for len(m.Menu.tags) <= i {
|
||||
m.Menu.tags = append(m.Menu.tags, false)
|
||||
}
|
||||
// handle pointer events for this item
|
||||
for _, ev := range gtx.Events(&m.Menu.tags[i]) {
|
||||
for {
|
||||
ev, ok := gtx.Event(pointer.Filter{
|
||||
Target: &m.Menu.tags[i],
|
||||
Kinds: pointer.Press | pointer.Enter | pointer.Leave,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
m.Menu.clicks = append(m.Menu.clicks, i)
|
||||
item.Doer.Do()
|
||||
m.Menu.Visible = false
|
||||
case pointer.Enter:
|
||||
m.Menu.hover = i + 1
|
||||
@ -89,17 +99,17 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
var macro op.MacroOp
|
||||
item := &items[i]
|
||||
if i == m.Menu.hover-1 && !item.Disabled {
|
||||
if i == m.Menu.hover-1 && item.Doer.Allowed() {
|
||||
macro = op.Record(gtx.Ops)
|
||||
}
|
||||
icon := widgetForIcon(item.IconBytes)
|
||||
iconColor := m.IconColor
|
||||
if item.Disabled {
|
||||
if !item.Doer.Allowed() {
|
||||
iconColor = mediumEmphasisTextColor
|
||||
}
|
||||
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
|
||||
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
|
||||
if item.Disabled {
|
||||
if !item.Doer.Allowed() {
|
||||
textLabel.Color = mediumEmphasisTextColor
|
||||
}
|
||||
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
|
||||
@ -118,19 +128,17 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
|
||||
}),
|
||||
)
|
||||
if i == m.Menu.hover-1 && !item.Disabled {
|
||||
if i == m.Menu.hover-1 && item.Doer.Allowed() {
|
||||
recording := macro.Stop()
|
||||
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
|
||||
Max: image.Pt(dims.Size.X, dims.Size.Y),
|
||||
}.Op())
|
||||
recording.Add(gtx.Ops)
|
||||
}
|
||||
if !item.Disabled {
|
||||
if item.Doer.Allowed() {
|
||||
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &m.Menu.tags[i],
|
||||
Types: pointer.Press | pointer.Enter | pointer.Leave,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &m.Menu.tags[i])
|
||||
area.Pop()
|
||||
}
|
||||
return dims
|
||||
@ -148,7 +156,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
return popup.Layout(gtx, contents)
|
||||
}
|
||||
|
||||
func (t *Tracker) PopupMenu(menu *Menu) MenuStyle {
|
||||
func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle {
|
||||
return MenuStyle{
|
||||
Menu: menu,
|
||||
IconColor: white,
|
||||
@ -157,6 +165,26 @@ func (t *Tracker) PopupMenu(menu *Menu) MenuStyle {
|
||||
FontSize: unit.Sp(16),
|
||||
IconSize: unit.Dp(16),
|
||||
HoverColor: menuHoverColor,
|
||||
Shaper: t.TextShaper,
|
||||
Shaper: shaper,
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *Tracker) layoutMenu(gtx C, title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
|
||||
for clickable.Clicked(gtx) {
|
||||
menu.Visible = true
|
||||
}
|
||||
m := PopupMenu(menu, tr.Theme.Shaper)
|
||||
return func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
titleBtn := material.Button(tr.Theme, clickable, title)
|
||||
titleBtn.Color = white
|
||||
titleBtn.Background = transparent
|
||||
titleBtn.CornerRadius = unit.Dp(0)
|
||||
dims := titleBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.X = gtx.Dp(width)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(300))
|
||||
m.Layout(gtx, items...)
|
||||
return dims
|
||||
}
|
||||
}
|
||||
|
||||
384
tracker/gioui/note_editor.go
Normal file
384
tracker/gioui/note_editor.go
Normal file
@ -0,0 +1,384 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
const trackRowHeight = unit.Dp(16)
|
||||
const trackColWidth = unit.Dp(54)
|
||||
const trackColTitleHeight = unit.Dp(16)
|
||||
const trackPatMarkWidth = unit.Dp(25)
|
||||
const trackRowMarkWidth = unit.Dp(25)
|
||||
|
||||
var noteStr [256]string
|
||||
var hexStr [256]string
|
||||
|
||||
func init() {
|
||||
// initialize these strings once, so we don't have to do it every time we draw the note editor
|
||||
hexStr[0] = "--"
|
||||
hexStr[1] = ".."
|
||||
noteStr[0] = "---"
|
||||
noteStr[1] = "..."
|
||||
for i := 2; i < 256; i++ {
|
||||
hexStr[i] = fmt.Sprintf("%02x", i)
|
||||
oNote := mod(i-baseNote, 12)
|
||||
octave := (i - oNote - baseNote) / 12
|
||||
switch {
|
||||
case octave < 0:
|
||||
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
||||
case octave >= 10:
|
||||
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
||||
default:
|
||||
noteStr[i] = fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type NoteEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *ActionClickable
|
||||
DeleteTrackBtn *ActionClickable
|
||||
AddSemitoneBtn *ActionClickable
|
||||
SubtractSemitoneBtn *ActionClickable
|
||||
AddOctaveBtn *ActionClickable
|
||||
SubtractOctaveBtn *ActionClickable
|
||||
NoteOffBtn *ActionClickable
|
||||
EffectBtn *BoolClickable
|
||||
|
||||
scrollTable *ScrollTable
|
||||
tag struct{}
|
||||
}
|
||||
|
||||
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
return &NoteEditor{
|
||||
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
||||
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
||||
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
||||
AddSemitoneBtn: NewActionClickable(model.AddSemitone()),
|
||||
SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()),
|
||||
AddOctaveBtn: NewActionClickable(model.AddOctave()),
|
||||
SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()),
|
||||
NoteOffBtn: NewActionClickable(model.EditNoteOff()),
|
||||
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
||||
scrollTable: NewScrollTable(
|
||||
model.Notes().Table(),
|
||||
model.Tracks().List(),
|
||||
model.NoteRows().List(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.Filter{Focus: te.scrollTable, Name: "A"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "B"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "C"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "D"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "E"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "F"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "G"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "H"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "I"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "J"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "K"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "L"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "M"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "N"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "O"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "P"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "Q"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "R"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "S"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "T"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "U"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "V"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "W"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "X"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "Y"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "Z"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "0"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "1"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "2"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "3"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "4"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "5"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "6"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "7"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "8"},
|
||||
key.Filter{Focus: te.scrollTable, Name: "9"},
|
||||
key.Filter{Focus: te.scrollTable, Name: ","},
|
||||
key.Filter{Focus: te.scrollTable, Name: "."},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State == key.Release {
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
noteID.NoteOff()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
te.command(gtx, t, e)
|
||||
}
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
|
||||
return Surface{Gray: 24, Focus: te.scrollTable.Focused()}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return te.layoutButtons(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return te.layoutTracks(gtx, t)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
return Surface{Gray: 37, Focus: te.scrollTable.Focused() || te.scrollTable.ChildFocused(), FitSize: true}.Layout(gtx, func(gtx C) D {
|
||||
addSemitoneBtnStyle := ActionButton(gtx, t.Theme, te.AddSemitoneBtn, "+1")
|
||||
subtractSemitoneBtnStyle := ActionButton(gtx, t.Theme, te.SubtractSemitoneBtn, "-1")
|
||||
addOctaveBtnStyle := ActionButton(gtx, t.Theme, te.AddOctaveBtn, "+12")
|
||||
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12")
|
||||
noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)")
|
||||
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, "Add track\n(Ctrl+T)")
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(addOctaveBtnStyle.Layout),
|
||||
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
||||
layout.Rigid(noteOffBtnStyle.Layout),
|
||||
layout.Rigid(effectBtnStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
})
|
||||
}
|
||||
|
||||
const baseNote = 24
|
||||
|
||||
var notes = []string{
|
||||
"C-",
|
||||
"C#",
|
||||
"D-",
|
||||
"D#",
|
||||
"E-",
|
||||
"F-",
|
||||
"F#",
|
||||
"G-",
|
||||
"G#",
|
||||
"A-",
|
||||
"A#",
|
||||
"B-",
|
||||
}
|
||||
|
||||
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
|
||||
beatMarkerDensity := t.RowsPerBeat().Value()
|
||||
switch beatMarkerDensity {
|
||||
case 0, 1, 2:
|
||||
beatMarkerDensity = 4
|
||||
}
|
||||
|
||||
playSongRow := t.PlaySongRow()
|
||||
pxWidth := gtx.Dp(trackColWidth)
|
||||
pxHeight := gtx.Dp(trackRowHeight)
|
||||
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
||||
pxRowMarkWidth := gtx.Dp(trackRowMarkWidth)
|
||||
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(unit.Dp(trackColTitleHeight))
|
||||
title := ((*tracker.Order)(t.Model)).Title(i)
|
||||
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
return D{Size: image.Pt(pxWidth, h)}
|
||||
}
|
||||
|
||||
rowTitleBg := func(gtx C, j int) D {
|
||||
if mod(j, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
} else if mod(j, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
}
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && j == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
}
|
||||
return D{}
|
||||
}
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
||||
pat := j / rpp
|
||||
row := j % rpp
|
||||
w := pxPatMarkWidth + pxRowMarkWidth
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
if row == 0 {
|
||||
color := rowMarkerPatternTextColor
|
||||
if l := t.Loop(); pat >= l.Start && pat < l.Start+l.Length {
|
||||
color = loopMarkerColor
|
||||
}
|
||||
paint.ColorOp{Color: color}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", pat)), op.CallOp{})
|
||||
}
|
||||
defer op.Offset(image.Pt(pxPatMarkWidth, 0)).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", row)), op.CallOp{})
|
||||
return D{Size: image.Pt(w, pxHeight)}
|
||||
}
|
||||
|
||||
drawSelection := te.scrollTable.Table.Cursor() != te.scrollTable.Table.Cursor2()
|
||||
selection := te.scrollTable.Table.Range()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
// draw the background, to indicate selection
|
||||
color := transparent
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if drawSelection && selection.Contains(point) {
|
||||
color = inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
color = selectionColor
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
// draw the cursor
|
||||
if point == te.scrollTable.Table.Cursor() {
|
||||
cw := gtx.Constraints.Min.X
|
||||
cx := 0
|
||||
if t.Model.Notes().Effect(x) {
|
||||
cw /= 2
|
||||
if t.Model.Notes().LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
c := inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
c = cursorColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
|
||||
}
|
||||
// draw the pattern marker
|
||||
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
||||
pat := y / rpp
|
||||
row := y % rpp
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
s := t.Model.Order().Value(tracker.Point{X: x, Y: pat})
|
||||
if row == 0 { // draw the pattern marker
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
||||
}
|
||||
if row == 1 && t.Model.Notes().Unique(x, s) { // draw a * if the pattern is unique
|
||||
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
||||
}
|
||||
if te.scrollTable.Table.Cursor() == point && te.scrollTable.Focused() {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
val := noteStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
if t.Model.Notes().Effect(x) {
|
||||
val = hexStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
}
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
||||
return D{Size: image.Pt(pxWidth, pxHeight)}
|
||||
}
|
||||
table := FilledScrollTable(t.Theme, te.scrollTable, cell, colTitle, rowTitle, nil, rowTitleBg)
|
||||
table.RowTitleWidth = trackPatMarkWidth + trackRowMarkWidth
|
||||
table.ColumnTitleHeight = trackColTitleHeight
|
||||
table.CellWidth = trackColWidth
|
||||
table.CellHeight = trackRowHeight
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func mod(x, d int) int {
|
||||
x = x % d
|
||||
if x >= 0 {
|
||||
return x
|
||||
}
|
||||
if d < 0 {
|
||||
return x - d
|
||||
}
|
||||
return x + d
|
||||
}
|
||||
|
||||
func noteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
|
||||
if e.Name == "A" || e.Name == "1" {
|
||||
t.Model.Notes().Table().Fill(0)
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
return
|
||||
}
|
||||
var n byte
|
||||
if t.Model.Notes().Effect(te.scrollTable.Table.Cursor().X) {
|
||||
if nibbleValue, err := strconv.ParseInt(string(e.Name), 16, 8); err == nil {
|
||||
n = t.Model.Notes().Value(te.scrollTable.Table.Cursor())
|
||||
t.Model.Notes().FillNibble(byte(nibbleValue), t.Model.Notes().LowNibble())
|
||||
goto validNote
|
||||
}
|
||||
} else {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||
t.Model.Notes().Table().Fill(int(n))
|
||||
goto validNote
|
||||
}
|
||||
}
|
||||
return
|
||||
validNote:
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
trk := te.scrollTable.Table.Cursor().X
|
||||
t.KeyPlaying[e.Name] = t.TrackNoteOn(trk, n)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
case "+":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
te.AddOctaveBtn.Action.Do()
|
||||
} else {
|
||||
te.AddSemitoneBtn.Action.Do()
|
||||
}
|
||||
case "-":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
te.SubtractSemitoneBtn.Action.Do()
|
||||
} else {
|
||||
te.SubtractOctaveBtn.Action.Do()
|
||||
}
|
||||
}*/
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
|
||||
"gioui.org/font"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"gioui.org/x/component"
|
||||
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@ -23,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
type NumberInput struct {
|
||||
Value int
|
||||
Int tracker.Int
|
||||
dragStartValue int
|
||||
dragStartXY float32
|
||||
clickDecrease gesture.Click
|
||||
@ -33,8 +35,6 @@ type NumberInput struct {
|
||||
|
||||
type NumericUpDownStyle struct {
|
||||
NumberInput *NumberInput
|
||||
Min int
|
||||
Max int
|
||||
Color color.NRGBA
|
||||
Font font.Font
|
||||
TextSize unit.Sp
|
||||
@ -51,15 +51,17 @@ type NumericUpDownStyle struct {
|
||||
shaper text.Shaper
|
||||
}
|
||||
|
||||
func NumericUpDown(th *material.Theme, number *NumberInput, min, max int, tooltip string) NumericUpDownStyle {
|
||||
func NewNumberInput(v tracker.Int) *NumberInput {
|
||||
return &NumberInput{Int: v}
|
||||
}
|
||||
|
||||
func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) NumericUpDownStyle {
|
||||
bgColor := th.Palette.Fg
|
||||
bgColor.R /= 4
|
||||
bgColor.G /= 4
|
||||
bgColor.B /= 4
|
||||
return NumericUpDownStyle{
|
||||
NumberInput: number,
|
||||
Min: min,
|
||||
Max: max,
|
||||
Color: white,
|
||||
BorderColor: th.Palette.Fg,
|
||||
IconColor: th.Palette.ContrastFg,
|
||||
@ -104,12 +106,6 @@ func (s *NumericUpDownStyle) actualLayout(gtx C) D {
|
||||
layout.Flexed(1, s.layoutText),
|
||||
layout.Rigid(s.button(gtx.Constraints.Max.Y, widgetForIcon(icons.NavigationArrowForward), 1, &s.NumberInput.clickIncrease)),
|
||||
)
|
||||
if s.NumberInput.Value < s.Min {
|
||||
s.NumberInput.Value = s.Min
|
||||
}
|
||||
if s.NumberInput.Value > s.Max {
|
||||
s.NumberInput.Value = s.Max
|
||||
}
|
||||
off.Pop()
|
||||
c2.Pop()
|
||||
return layout.Dimensions{Size: size}
|
||||
@ -156,7 +152,7 @@ func (s *NumericUpDownStyle) layoutText(gtx C) D {
|
||||
}),
|
||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
||||
paint.ColorOp{Color: s.Color}.Add(gtx.Ops)
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, &s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Value), op.CallOp{})
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, &s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Int.Value()), op.CallOp{})
|
||||
}),
|
||||
layout.Expanded(s.layoutDrag),
|
||||
)
|
||||
@ -165,17 +161,24 @@ func (s *NumericUpDownStyle) layoutText(gtx C) D {
|
||||
func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
|
||||
{ // handle dragging
|
||||
pxPerStep := float32(gtx.Dp(s.UnitsPerStep))
|
||||
for _, ev := range gtx.Events(s.NumberInput) {
|
||||
for {
|
||||
ev, ok := gtx.Event(pointer.Filter{
|
||||
Target: s.NumberInput,
|
||||
Kinds: pointer.Press | pointer.Drag | pointer.Release,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := ev.(pointer.Event); ok {
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
s.NumberInput.dragStartValue = s.NumberInput.Value
|
||||
s.NumberInput.dragStartValue = s.NumberInput.Int.Value()
|
||||
s.NumberInput.dragStartXY = e.Position.X - e.Position.Y
|
||||
|
||||
case pointer.Drag:
|
||||
var deltaCoord float32
|
||||
deltaCoord = e.Position.X - e.Position.Y - s.NumberInput.dragStartXY
|
||||
s.NumberInput.Value = s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5)
|
||||
s.NumberInput.Int.Set(s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,10 +188,7 @@ func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
|
||||
// register for input
|
||||
dragRect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
||||
area := clip.Rect(dragRect).Push(gtx.Ops)
|
||||
pointer.InputOp{
|
||||
Tag: s.NumberInput,
|
||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, s.NumberInput)
|
||||
area.Pop()
|
||||
stack.Pop()
|
||||
}
|
||||
@ -197,10 +197,14 @@ func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
func (s *NumericUpDownStyle) layoutClick(gtx layout.Context, delta int, click *gesture.Click) layout.Dimensions {
|
||||
// handle clicking
|
||||
for _, e := range click.Events(gtx) {
|
||||
switch e.Type {
|
||||
case gesture.TypeClick:
|
||||
s.NumberInput.Value += delta
|
||||
for {
|
||||
ev, ok := click.Update(gtx.Source)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch ev.Kind {
|
||||
case gesture.KindClick:
|
||||
s.NumberInput.Int.Add(delta)
|
||||
}
|
||||
}
|
||||
// Avoid affecting the input tree with pointer events.
|
||||
|
||||
213
tracker/gioui/order_editor.go
Normal file
213
tracker/gioui/order_editor.go
Normal file
@ -0,0 +1,213 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const patternCellHeight = unit.Dp(16)
|
||||
const patternCellWidth = unit.Dp(16)
|
||||
const patternRowMarkerWidth = unit.Dp(30)
|
||||
const orderTitleHeight = unit.Dp(52)
|
||||
|
||||
type OrderEditor struct {
|
||||
scrollTable *ScrollTable
|
||||
tag struct{}
|
||||
}
|
||||
|
||||
var patternIndexStrings [36]string
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
patternIndexStrings[i] = string('0' + byte(i))
|
||||
}
|
||||
for i := 10; i < 36; i++ {
|
||||
patternIndexStrings[i] = string('A' + byte(i-10))
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrderEditor(m *tracker.Model) *OrderEditor {
|
||||
return &OrderEditor{
|
||||
scrollTable: NewScrollTable(
|
||||
m.Order().Table(),
|
||||
m.Tracks().List(),
|
||||
m.OrderRows().List(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
|
||||
if oe.scrollTable.CursorMoved() {
|
||||
cursor := t.TrackEditor.scrollTable.Table.Cursor()
|
||||
t.TrackEditor.scrollTable.ColTitleList.CenterOn(cursor.X)
|
||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(cursor.Y)
|
||||
}
|
||||
|
||||
oe.handleEvents(gtx, t)
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
event.Op(gtx.Ops, &oe.tag)
|
||||
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(orderTitleHeight)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
|
||||
title := t.Model.Order().Title(i)
|
||||
LabelStyle{Alignment: layout.NW, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
return D{Size: image.Pt(gtx.Dp(patternCellWidth), h)}
|
||||
}
|
||||
|
||||
rowTitleBg := func(gtx C, j int) D {
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && j == t.PlayPosition().OrderRow {
|
||||
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Dp(patternCellHeight))}.Op())
|
||||
}
|
||||
return D{}
|
||||
}
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
w := gtx.Dp(unit.Dp(30))
|
||||
color := rowMarkerPatternTextColor
|
||||
if l := t.Loop(); j >= l.Start && j < l.Start+l.Length {
|
||||
color = loopMarkerColor
|
||||
}
|
||||
paint.ColorOp{Color: color}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
return D{Size: image.Pt(w, gtx.Dp(patternCellHeight))}
|
||||
}
|
||||
|
||||
selection := oe.scrollTable.Table.Range()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
val := patternIndexToString(t.Model.Order().Value(tracker.Point{X: x, Y: y}))
|
||||
color := patternCellColor
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if selection.Contains(point) {
|
||||
color = inactiveSelectionColor
|
||||
if oe.scrollTable.Focused() {
|
||||
color = selectionColor
|
||||
if point == oe.scrollTable.Table.Cursor() {
|
||||
color = cursorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(gtx.Constraints.Min.X-1, gtx.Constraints.Min.X-1)}.Op())
|
||||
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
||||
return D{Size: image.Pt(gtx.Dp(patternCellWidth), gtx.Dp(patternCellHeight))}
|
||||
}
|
||||
|
||||
table := FilledScrollTable(t.Theme, oe.scrollTable, cell, colTitle, rowTitle, nil, rowTitleBg)
|
||||
table.ColumnTitleHeight = orderTitleHeight
|
||||
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) handleEvents(gtx C, t *Tracker) {
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.Filter{Focus: oe.scrollTable, Name: key.NameDeleteBackward, Required: key.ModShortcut},
|
||||
key.Filter{Focus: oe.scrollTable, Name: key.NameDeleteForward, Required: key.ModShortcut},
|
||||
key.Filter{Focus: oe.scrollTable, Name: key.NameReturn, Optional: key.ModShortcut},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "0"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "1"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "2"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "3"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "4"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "5"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "6"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "7"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "8"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "9"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "A"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "B"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "C"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "D"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "E"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "F"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "G"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "H"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "I"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "J"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "K"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "L"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "M"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "N"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "O"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "P"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "Q"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "R"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "S"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "T"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "U"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "V"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "W"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "X"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "Y"},
|
||||
key.Filter{Focus: oe.scrollTable, Name: "Z"},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := e.(key.Event); ok {
|
||||
if e.State != key.Press {
|
||||
continue
|
||||
}
|
||||
oe.command(gtx, t, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) command(gtx C, t *Tracker, e key.Event) {
|
||||
switch e.Name {
|
||||
case key.NameDeleteBackward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(true).Do()
|
||||
}
|
||||
case key.NameDeleteForward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(false).Do()
|
||||
}
|
||||
case key.NameReturn:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
oe.scrollTable.Table.MoveCursor(0, -1)
|
||||
oe.scrollTable.Table.SetCursor2(oe.scrollTable.Table.Cursor())
|
||||
}
|
||||
t.Model.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut)).Do()
|
||||
}
|
||||
if iv, err := strconv.Atoi(string(e.Name)); err == nil {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv)
|
||||
oe.scrollTable.EnsureCursorVisible()
|
||||
}
|
||||
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), b+10)
|
||||
oe.scrollTable.EnsureCursorVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func patternIndexToString(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
} else if index < len(patternIndexStrings) {
|
||||
return patternIndexStrings[index]
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
@ -1,262 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const patternCellHeight = 16
|
||||
const patternCellWidth = 16
|
||||
const patternRowMarkerWidth = 30
|
||||
|
||||
type OrderEditor struct {
|
||||
list *layout.List
|
||||
titleList *DragList
|
||||
scrollBar *ScrollBar
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func NewOrderEditor() *OrderEditor {
|
||||
return &OrderEditor{
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
titleList: &DragList{List: &layout.List{Axis: layout.Horizontal}},
|
||||
scrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
}
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Focus() {
|
||||
oe.requestFocus = true
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Focused() bool {
|
||||
return oe.focused
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
|
||||
return Surface{Gray: 24, Focus: oe.focused}.Layout(gtx, func(gtx C) D {
|
||||
return oe.doLayout(gtx, t)
|
||||
})
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) doLayout(gtx C, t *Tracker) D {
|
||||
for _, e := range gtx.Events(&oe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
oe.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
case key.Event:
|
||||
if e.State != key.Press {
|
||||
continue
|
||||
}
|
||||
switch e.Name {
|
||||
case key.NameDeleteForward, key.NameDeleteBackward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.DeleteOrderRow(e.Name == key.NameDeleteForward)
|
||||
} else {
|
||||
t.DeletePatternSelection()
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
case "Space":
|
||||
if !t.Playing() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().ScoreRow
|
||||
startRow.Row = 0
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case key.NameReturn:
|
||||
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
|
||||
case key.NameUpArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.ScoreRow = tracker.ScoreRow{}
|
||||
} else {
|
||||
cursor.Row -= t.Song().Score.RowsPerPattern
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
t.SetCursor(cursor)
|
||||
case key.NameDownArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row = t.Song().Score.LengthInRows() - 1
|
||||
} else {
|
||||
cursor.Row += t.Song().Score.RowsPerPattern
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
t.SetCursor(cursor)
|
||||
case key.NameLeftArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track = 0
|
||||
} else {
|
||||
cursor.Track--
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
case key.NameRightArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track = len(t.Song().Score.Tracks) - 1
|
||||
} else {
|
||||
cursor.Track++
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
case "+":
|
||||
t.AdjustPatternNumber(1, e.Modifiers.Contain(key.ModShortcut))
|
||||
continue
|
||||
case "-":
|
||||
t.AdjustPatternNumber(-1, e.Modifiers.Contain(key.ModShortcut))
|
||||
continue
|
||||
case key.NameHome:
|
||||
cursor := t.Cursor()
|
||||
cursor.Track = 0
|
||||
t.SetCursor(cursor)
|
||||
case key.NameEnd:
|
||||
cursor := t.Cursor()
|
||||
cursor.Track = len(t.Song().Score.Tracks) - 1
|
||||
t.SetCursor(cursor)
|
||||
}
|
||||
if (e.Name != key.NameLeftArrow &&
|
||||
e.Name != key.NameRightArrow &&
|
||||
e.Name != key.NameUpArrow &&
|
||||
e.Name != key.NameDownArrow) ||
|
||||
!e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
continue
|
||||
}
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.SetCurrentPattern(iv)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.SetCurrentPattern(b + 10)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
if oe.requestFocus {
|
||||
oe.requestFocus = false
|
||||
key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &oe.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
|
||||
key.InputOp{Tag: &oe.tag, Keys: "←|→|↑|↓|Shift-←|Shift-→|Shift-↑|Shift-↓|⏎|⇱|⇲|⌫|⌦|Ctrl-⌫|Ctrl-⌦|+|-|Space|0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z"}.Add(gtx.Ops)
|
||||
|
||||
patternRect := tracker.ScoreRect{
|
||||
Corner1: tracker.ScorePoint{ScoreRow: tracker.ScoreRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track},
|
||||
Corner2: tracker.ScorePoint{ScoreRow: tracker.ScoreRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track},
|
||||
}
|
||||
|
||||
// draw the single letter titles for tracks
|
||||
{
|
||||
gtx := gtx
|
||||
stack := op.Offset(image.Pt(patternRowMarkerWidth, 0)).Push(gtx.Ops)
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-patternRowMarkerWidth, patternCellHeight))
|
||||
elem := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(patternCellWidth, patternCellHeight))
|
||||
instr, err := t.Song().Patch.InstrumentForVoice(t.Song().Score.FirstVoiceForTrack(i))
|
||||
var title string
|
||||
if err == nil && len(t.Song().Patch[instr].Name) > 0 {
|
||||
title = string(t.Song().Patch[instr].Name[0])
|
||||
} else {
|
||||
title = "?"
|
||||
}
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.TextShaper}.Layout(gtx)
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
style := FilledDragList(t.Theme, oe.titleList, len(t.Song().Score.Tracks), elem, t.SwapTracks)
|
||||
style.HoverColor = transparent
|
||||
style.SelectedColor = transparent
|
||||
style.Layout(gtx)
|
||||
stack.Pop()
|
||||
}
|
||||
op.Offset(image.Pt(0, patternCellHeight)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y -= patternCellHeight
|
||||
gtx.Constraints.Min.Y -= patternCellHeight
|
||||
element := func(gtx C, j int) D {
|
||||
if playPos := t.PlayPosition(); t.Playing() && j == playPos.Pattern {
|
||||
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
|
||||
}
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
stack := op.Offset(image.Pt(patternRowMarkerWidth, 0)).Push(gtx.Ops)
|
||||
for i, track := range t.Song().Score.Tracks {
|
||||
paint.FillShape(gtx.Ops, patternCellColor, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(patternCellWidth-1, patternCellHeight-1)}.Op())
|
||||
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
|
||||
if j >= 0 && j < len(track.Order) && track.Order[j] >= 0 {
|
||||
gtx := gtx
|
||||
gtx.Constraints.Max.X = patternCellWidth
|
||||
op.Offset(image.Pt(0, -2)).Add(gtx.Ops)
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j]), op.CallOp{})
|
||||
op.Offset(image.Pt(0, 2)).Add(gtx.Ops)
|
||||
}
|
||||
point := tracker.ScorePoint{Track: i, ScoreRow: tracker.ScoreRow{Pattern: j}}
|
||||
if oe.focused || t.TrackEditor.Focused() {
|
||||
if patternRect.Contains(point) {
|
||||
color := inactiveSelectionColor
|
||||
if oe.focused {
|
||||
color = selectionColor
|
||||
if point.Pattern == t.Cursor().Pattern && point.Track == t.Cursor().Track {
|
||||
color = cursorColor
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op())
|
||||
}
|
||||
}
|
||||
op.Offset(image.Pt(patternCellWidth, 0)).Add(gtx.Ops)
|
||||
}
|
||||
stack.Pop()
|
||||
return D{Size: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}
|
||||
}
|
||||
|
||||
return layout.Stack{Alignment: layout.NE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return oe.list.Layout(gtx, t.Song().Score.Length, element)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return oe.scrollBar.Layout(gtx, unit.Dp(10), t.Song().Score.Length, &oe.list.Position)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func patternIndexToString(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
} else if index < 10 {
|
||||
return string('0' + byte(index))
|
||||
}
|
||||
return string('A' + byte(index-10))
|
||||
}
|
||||
@ -1,256 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ParamEditor struct {
|
||||
list *layout.List
|
||||
scrollBar *ScrollBar
|
||||
Parameters []*ParameterWidget
|
||||
DeleteUnitBtn *TipClickable
|
||||
CopyUnitBtn *TipClickable
|
||||
ClearUnitBtn *TipClickable
|
||||
ChooseUnitTypeBtns []*widget.Clickable
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) Focus() {
|
||||
pe.requestFocus = true
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) Focused() bool {
|
||||
return pe.focused
|
||||
}
|
||||
|
||||
func NewParamEditor() *ParamEditor {
|
||||
ret := &ParamEditor{
|
||||
DeleteUnitBtn: new(TipClickable),
|
||||
ClearUnitBtn: new(TipClickable),
|
||||
CopyUnitBtn: new(TipClickable),
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
scrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
}
|
||||
for range sointu.UnitNames {
|
||||
ret.ChooseUnitTypeBtns = append(ret.ChooseUnitTypeBtns, new(widget.Clickable))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) Bind(t *Tracker) layout.Widget {
|
||||
return func(gtx C) D {
|
||||
for _, e := range gtx.Events(&pe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
pe.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
case key.Event:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
continue
|
||||
}
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameUpArrow:
|
||||
t.SetParamIndex(t.ParamIndex() - 1)
|
||||
case key.NameDownArrow:
|
||||
t.SetParamIndex(t.ParamIndex() + 1)
|
||||
case key.NameLeftArrow:
|
||||
p, err := t.Param(t.ParamIndex())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetParam(p.Value - p.LargeStep)
|
||||
} else {
|
||||
t.SetParam(p.Value - 1)
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
p, err := t.Param(t.ParamIndex())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetParam(p.Value + p.LargeStep)
|
||||
} else {
|
||||
t.SetParam(p.Value + 1)
|
||||
}
|
||||
case key.NameEscape:
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if pe.requestFocus {
|
||||
pe.requestFocus = false
|
||||
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
editorFunc := pe.layoutUnitSliders
|
||||
if y := t.Unit().Type; y == "" || y != t.InstrumentEditor.unitTypeEditor.Text() {
|
||||
editorFunc = pe.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return editorFunc(gtx, t)
|
||||
}),
|
||||
layout.Rigid(pe.layoutUnitFooter(t)))
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &pe.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
key.InputOp{Tag: &pe.tag, Keys: "←|Shift-←|→|Shift-→|↑|↓|⎋"}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
return ret
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) layoutUnitSliders(gtx C, t *Tracker) D {
|
||||
numItems := t.NumParams()
|
||||
|
||||
for len(pe.Parameters) <= numItems {
|
||||
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
|
||||
}
|
||||
|
||||
listItem := func(gtx C, index int) D {
|
||||
for pe.Parameters[index].Clicked() {
|
||||
if t.ParamIndex() != index {
|
||||
t.SetParamIndex(index)
|
||||
} else {
|
||||
t.ResetParam()
|
||||
}
|
||||
pe.Focus()
|
||||
}
|
||||
param, err := t.Param(index)
|
||||
if err != nil {
|
||||
return D{}
|
||||
}
|
||||
oldVal := param.Value
|
||||
paramStyle := t.ParamStyle(t.Theme, ¶m, pe.Parameters[index])
|
||||
paramStyle.Focus = pe.focused && t.ParamIndex() == index
|
||||
dims := paramStyle.Layout(gtx)
|
||||
if oldVal != param.Value {
|
||||
pe.Focus()
|
||||
t.SetParamIndex(index)
|
||||
t.SetParam(param.Value)
|
||||
}
|
||||
return dims
|
||||
}
|
||||
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return pe.list.Layout(gtx, numItems, listItem)
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
return pe.scrollBar.Layout(gtx, unit.Dp(10), numItems, &pe.list.Position)
|
||||
}))
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) layoutUnitFooter(t *Tracker) layout.Widget {
|
||||
return func(gtx C) D {
|
||||
for pe.ClearUnitBtn.Clickable.Clicked() {
|
||||
t.SetUnitType("")
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
for pe.DeleteUnitBtn.Clickable.Clicked() {
|
||||
t.DeleteUnits(false, t.UnitIndex(), t.UnitIndex())
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
for pe.CopyUnitBtn.Clickable.Clicked() {
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
contents, err := yaml.Marshal([]sointu.Unit{t.Unit()})
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
copyUnitBtnStyle := IconButton(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, true, "Copy unit (Ctrl+C)")
|
||||
deleteUnitBtnStyle := IconButton(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, t.CanDeleteUnit(), "Delete unit (Del)")
|
||||
text := t.Unit().Type
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = strings.Title(text)
|
||||
}
|
||||
hintText := Label(text, white, t.TextShaper)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(copyUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Unit().Type != "" {
|
||||
clearUnitBtnStyle := IconButton(t.Theme, pe.ClearUnitBtn, icons.ContentClear, true, "Clear unit")
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D {
|
||||
listElem := func(gtx C, i int) D {
|
||||
for pe.ChooseUnitTypeBtns[i].Clicked() {
|
||||
t.SetUnitType(sointu.UnitNames[i])
|
||||
t.InstrumentEditor.unitTypeEditor.SetText(sointu.UnitNames[i])
|
||||
}
|
||||
text := sointu.UnitNames[i]
|
||||
if t.InstrumentEditor.unitTypeEditor.Focused() && !strings.HasPrefix(text, t.InstrumentEditor.unitTypeEditor.Text()) {
|
||||
return D{}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
bg := func(gtx C) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 20))
|
||||
var color color.NRGBA
|
||||
if pe.ChooseUnitTypeBtns[i].Hovered() {
|
||||
color = unitTypeListHighlightColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
leftMargin := layout.Inset{Left: unit.Dp(10)}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Stacked(bg),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return pe.ChooseUnitTypeBtns[i].Layout(gtx, func(gtx C) D {
|
||||
return leftMargin.Layout(gtx, labelStyle.Layout)
|
||||
})
|
||||
}))
|
||||
}
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return pe.list.Layout(gtx, len(sointu.UnitNames), listElem)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return pe.scrollBar.Layout(gtx, unit.Dp(10), len(sointu.UnitNames), &pe.list.Position)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@ -1,218 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
labelBtn widget.Clickable
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
Parameter *tracker.Parameter
|
||||
ParameterWidget *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, param *tracker.Parameter, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Parameter: param,
|
||||
Theme: th,
|
||||
ParameterWidget: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParameterWidget) Clicked() bool {
|
||||
return p.labelBtn.Clicked()
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.ParameterWidget.labelBtn.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.Parameter.Name, white, p.tracker.TextShaper))
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.Parameter.Type {
|
||||
case tracker.IntegerParameter:
|
||||
for _, e := range gtx.Events(&p.ParameterWidget.floatWidget) {
|
||||
switch ev := e.(type) {
|
||||
case pointer.Event:
|
||||
if ev.Type == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
p.Parameter.Value += int(math.Round(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
if !p.ParameterWidget.floatWidget.Dragging() {
|
||||
p.ParameterWidget.floatWidget.Value = float32(p.Parameter.Value)
|
||||
}
|
||||
sliderStyle := material.Slider(p.Theme, &p.ParameterWidget.floatWidget, float32(p.Parameter.Min), float32(p.Parameter.Max))
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
r := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
area := clip.Rect(r).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &p.ParameterWidget.floatWidget, Types: pointer.Scroll, ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)}}.Add(gtx.Ops)
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
area.Pop()
|
||||
p.Parameter.Value = int(p.ParameterWidget.floatWidget.Value + 0.5)
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
p.ParameterWidget.boolWidget.Value = p.Parameter.Value > p.Parameter.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.ParameterWidget.boolWidget, "Toggle boolean parameter")
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.ParameterWidget.boolWidget.Value {
|
||||
p.Parameter.Value = p.Parameter.Max
|
||||
} else {
|
||||
p.Parameter.Value = p.Parameter.Min
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
for clickedItem, hasClicked := p.ParameterWidget.instrMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = p.tracker.Song().Patch[clickedItem].Units[0].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.instrMenu.Clicked()
|
||||
}
|
||||
instrItems := make([]MenuItem, len(p.tracker.Song().Patch))
|
||||
for i, instr := range p.tracker.Song().Patch {
|
||||
instrItems[i].Text = instr.Name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.Song().Patch.FindUnit(p.Parameter.Value)
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Song().Patch[targetI]
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for clickedItem, hasClicked := p.ParameterWidget.unitMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = units[clickedItem].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.unitMenu.Clicked()
|
||||
}
|
||||
for j, unit := range units {
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(instrName, &p.ParameterWidget.instrBtn, &p.ParameterWidget.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(unitName, &p.ParameterWidget.unitBtn, &p.ParameterWidget.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.Parameter.Type != tracker.IDParameter {
|
||||
return Label(p.Parameter.Hint, white, p.tracker.TextShaper)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
func (t *Tracker) layoutParameter(gtx C, index int) D {
|
||||
u := t.Unit()
|
||||
ut, _ := sointu.UnitTypes[u.Type]
|
||||
|
||||
params := u.Parameters
|
||||
var name string
|
||||
var value, min, max int
|
||||
var valueText string
|
||||
if u.Type == "oscillator" && index == len(ut) {
|
||||
name = "sample"
|
||||
key := compiler.SampleOffset{Start: uint32(params["samplestart"]), LoopStart: uint16(params["loopstart"]), LoopLength: uint16(params["looplength"])}
|
||||
if v, ok := tracker.GmDlsEntryMap[key]; ok {
|
||||
value = v + 1
|
||||
valueText = fmt.Sprintf("%v / %v", value, tracker.GmDlsEntries[v].Name)
|
||||
} else {
|
||||
value = 0
|
||||
valueText = "0 / custom"
|
||||
}
|
||||
min, max = 0, len(tracker.GmDlsEntries)
|
||||
} else {
|
||||
if ut[index].MaxValue < ut[index].MinValue {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
name = ut[index].Name
|
||||
if u.Type == "oscillator" && (name == "samplestart" || name == "loopstart" || name == "looplength") {
|
||||
if params["type"] != sointu.Sample {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}
|
||||
value = params[name]
|
||||
min, max = ut[index].MinValue, ut[index].MaxValue
|
||||
if u.Type == "send" && name == "voice" {
|
||||
max = t.Song().Patch.NumVoices()
|
||||
} else if u.Type == "send" && name == "unit" { // set the maximum values depending on the send target
|
||||
instrIndex, _, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 {
|
||||
max = len(t.Song().Patch[instrIndex].Units) - 1
|
||||
}
|
||||
} else if u.Type == "send" && name == "port" { // set the maximum values depending on the send target
|
||||
instrIndex, unitIndex, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 && unitIndex != -1 {
|
||||
max = len(sointu.Ports[t.Song().Patch[instrIndex].Units[unitIndex].Type]) - 1
|
||||
}
|
||||
}
|
||||
hint := t.Song().Patch.ParamHintString(t.InstrIndex(), t.UnitIndex(), name)
|
||||
if hint != "" {
|
||||
valueText = fmt.Sprintf("%v / %v", value, hint)
|
||||
} else {
|
||||
valueText = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@ -44,13 +45,19 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D {
|
||||
return D{}
|
||||
}
|
||||
|
||||
for _, ev := range gtx.Events(s.Visible) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
for {
|
||||
event, ok := gtx.Event(pointer.Filter{
|
||||
Target: s.Visible,
|
||||
Kinds: pointer.Press,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := event.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
*s.Visible = false
|
||||
}
|
||||
@ -70,16 +77,10 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D {
|
||||
paint.FillShape(gtx.Ops, s.ShadowColor, rrect2.Op(gtx.Ops))
|
||||
paint.FillShape(gtx.Ops, s.SurfaceColor, rrect.Op(gtx.Ops))
|
||||
area := clip.Rect(image.Rect(-1e6, -1e6, 1e6, 1e6)).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: s.Visible,
|
||||
Types: pointer.Press,
|
||||
Grab: true,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, s.Visible)
|
||||
area.Pop()
|
||||
area = clip.Rect(rrect2.Rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &dummyTag,
|
||||
Types: pointer.Press,
|
||||
Grab: true,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &dummyTag)
|
||||
area.Pop()
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
|
||||
81
tracker/gioui/popup_alert.go
Normal file
81
tracker/gioui/popup_alert.go
Normal file
@ -0,0 +1,81 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type PopupAlert struct {
|
||||
alerts *tracker.Alerts
|
||||
prevUpdate time.Time
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
var alertSpeed = 150 * time.Millisecond
|
||||
var alertMargin = layout.UniformInset(unit.Dp(6))
|
||||
var alertInset = layout.UniformInset(unit.Dp(6))
|
||||
|
||||
func NewPopupAlert(alerts *tracker.Alerts, shaper *text.Shaper) *PopupAlert {
|
||||
return &PopupAlert{alerts: alerts, shaper: shaper, prevUpdate: time.Now()}
|
||||
}
|
||||
|
||||
func (a *PopupAlert) Layout(gtx C) D {
|
||||
now := time.Now()
|
||||
if a.alerts.Update(now.Sub(a.prevUpdate)) {
|
||||
gtx.Execute(op.InvalidateCmd{At: now.Add(50 * time.Millisecond)})
|
||||
}
|
||||
a.prevUpdate = now
|
||||
|
||||
var totalY float64
|
||||
a.alerts.Iterate(func(alert tracker.Alert) {
|
||||
var color, textColor, shadeColor color.NRGBA
|
||||
switch alert.Priority {
|
||||
case tracker.Warning:
|
||||
color = warningColor
|
||||
textColor = black
|
||||
case tracker.Error:
|
||||
color = errorColor
|
||||
textColor = black
|
||||
default:
|
||||
color = popupSurfaceColor
|
||||
textColor = white
|
||||
shadeColor = black
|
||||
}
|
||||
bgWidget := func(gtx C) D {
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: alert.Message, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
|
||||
alertMargin.Layout(gtx, func(gtx C) D {
|
||||
return layout.S.Layout(gtx, func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
recording := op.Record(gtx.Ops)
|
||||
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(bgWidget),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return alertInset.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
)
|
||||
macro := recording.Stop()
|
||||
delta := float64(dims.Size.Y + gtx.Dp(alertMargin.Bottom))
|
||||
op.Offset(image.Point{0, int(-totalY*alert.FadeLevel + delta*(1-alert.FadeLevel))}).Add((gtx.Ops))
|
||||
totalY += delta
|
||||
macro.Add(gtx.Ops)
|
||||
return dims
|
||||
})
|
||||
})
|
||||
})
|
||||
return D{}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
const rowMarkerWidth = 50
|
||||
|
||||
func (t *Tracker) layoutRowMarkers(gtx C) D {
|
||||
gtx.Constraints.Min.X = rowMarkerWidth
|
||||
paint.FillShape(gtx.Ops, rowMarkerSurfaceColor, clip.Rect{
|
||||
Max: gtx.Constraints.Max,
|
||||
}.Op())
|
||||
//defer op.Save(gtx.Ops).Load()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
op.Offset(image.Pt(0, (gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
playPos := t.PlayPosition()
|
||||
playSongRow := playPos.Pattern*t.Song().Score.RowsPerPattern + playPos.Row
|
||||
op.Offset(image.Pt(0, (-1*trackRowHeight)*(cursorSongRow))).Add(gtx.Ops)
|
||||
beatMarkerDensity := t.Song().RowsPerBeat
|
||||
for beatMarkerDensity <= 2 {
|
||||
beatMarkerDensity *= 2
|
||||
}
|
||||
for i := 0; i < t.Song().Score.Length; i++ {
|
||||
for j := 0; j < t.Song().Score.RowsPerPattern; j++ {
|
||||
songRow := i*t.Song().Score.RowsPerPattern + j
|
||||
if mod(songRow, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
} else if mod(songRow, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if t.Playing() && songRow == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if j == 0 {
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i)), op.CallOp{})
|
||||
}
|
||||
if t.TrackEditor.Focused() && songRow == cursorSongRow {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
op.Offset(image.Pt(rowMarkerWidth/2, 0)).Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
op.Offset(image.Pt(-rowMarkerWidth/2, trackRowHeight)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
return layout.Dimensions{Size: image.Pt(rowMarkerWidth, gtx.Constraints.Max.Y)}
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
m := a % b
|
||||
if a < 0 && b < 0 {
|
||||
m -= b
|
||||
}
|
||||
if a < 0 && b > 0 {
|
||||
m += b
|
||||
}
|
||||
return m
|
||||
}
|
||||
296
tracker/gioui/scroll_table.go
Normal file
296
tracker/gioui/scroll_table.go
Normal file
@ -0,0 +1,296 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type ScrollTable struct {
|
||||
ColTitleList *DragList
|
||||
RowTitleList *DragList
|
||||
Table tracker.Table
|
||||
focused bool
|
||||
requestFocus bool
|
||||
cursorMoved bool
|
||||
}
|
||||
|
||||
type ScrollTableStyle struct {
|
||||
RowTitleStyle FilledDragListStyle
|
||||
ColTitleStyle FilledDragListStyle
|
||||
ScrollTable *ScrollTable
|
||||
ScrollBarWidth unit.Dp
|
||||
RowTitleWidth unit.Dp
|
||||
ColumnTitleHeight unit.Dp
|
||||
CellWidth unit.Dp
|
||||
CellHeight unit.Dp
|
||||
element func(gtx C, x, y int) D
|
||||
}
|
||||
|
||||
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
|
||||
return &ScrollTable{
|
||||
Table: table,
|
||||
ColTitleList: NewDragList(vertList, layout.Horizontal),
|
||||
RowTitleList: NewDragList(horizList, layout.Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
func FilledScrollTable(th *material.Theme, scrollTable *ScrollTable, element func(gtx C, x, y int) D, colTitle, rowTitle, colTitleBg, rowTitleBg func(gtx C, i int) D) ScrollTableStyle {
|
||||
return ScrollTableStyle{
|
||||
RowTitleStyle: FilledDragList(th, scrollTable.RowTitleList, rowTitle, rowTitleBg),
|
||||
ColTitleStyle: FilledDragList(th, scrollTable.ColTitleList, colTitle, colTitleBg),
|
||||
ScrollTable: scrollTable,
|
||||
element: element,
|
||||
ScrollBarWidth: unit.Dp(14),
|
||||
RowTitleWidth: unit.Dp(30),
|
||||
ColumnTitleHeight: unit.Dp(16),
|
||||
CellWidth: unit.Dp(16),
|
||||
CellHeight: unit.Dp(16),
|
||||
}
|
||||
}
|
||||
|
||||
func (st *ScrollTable) CursorMoved() bool {
|
||||
ret := st.cursorMoved
|
||||
st.cursorMoved = false
|
||||
return ret
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focus() {
|
||||
st.requestFocus = true
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focused() bool {
|
||||
return st.focused
|
||||
}
|
||||
|
||||
func (st *ScrollTable) EnsureCursorVisible() {
|
||||
st.ColTitleList.EnsureVisible(st.Table.Cursor().X)
|
||||
st.RowTitleList.EnsureVisible(st.Table.Cursor().Y)
|
||||
}
|
||||
|
||||
func (st *ScrollTable) ChildFocused() bool {
|
||||
return st.ColTitleList.Focused() || st.RowTitleList.Focused()
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) Layout(gtx C) D {
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
event.Op(gtx.Ops, s.ScrollTable)
|
||||
|
||||
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
|
||||
s.handleEvents(gtx, p)
|
||||
|
||||
return Surface{Gray: 24, Focus: s.ScrollTable.Focused() || s.ScrollTable.ChildFocused()}.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()
|
||||
dims := gtx.Constraints.Max
|
||||
s.layoutColTitles(gtx, p)
|
||||
s.layoutRowTitles(gtx, p)
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
|
||||
s.layoutTable(gtx, p)
|
||||
s.RowTitleStyle.LayoutScrollBar(gtx)
|
||||
s.ColTitleStyle.LayoutScrollBar(gtx)
|
||||
return D{Size: dims}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.FocusFilter{Target: s.ScrollTable},
|
||||
transfer.TargetFilter{Target: s.ScrollTable, Type: "application/text"},
|
||||
pointer.Filter{Target: s.ScrollTable, Kinds: pointer.Press},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameLeftArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameUpArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameRightArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameDownArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NamePageUp, Optional: key.ModShift},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NamePageDown, Optional: key.ModShift},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameHome, Optional: key.ModShift},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameEnd, Optional: key.ModShift},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameDeleteBackward},
|
||||
key.Filter{Focus: s.ScrollTable, Name: key.NameDeleteForward},
|
||||
key.Filter{Focus: s.ScrollTable, Name: "C", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.ScrollTable, Name: "V", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.ScrollTable, Name: "X", Required: key.ModShortcut},
|
||||
key.Filter{Focus: s.ScrollTable, Name: "+"},
|
||||
key.Filter{Focus: s.ScrollTable, Name: "-"},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
s.ScrollTable.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if int(e.Position.X) < p.X || int(e.Position.Y) < p.Y {
|
||||
break
|
||||
}
|
||||
e.Position.X -= float32(p.X)
|
||||
e.Position.Y -= float32(p.Y)
|
||||
if e.Kind == pointer.Press {
|
||||
gtx.Execute(key.FocusCmd{Tag: s.ScrollTable})
|
||||
}
|
||||
dx := (int(e.Position.X) + s.ScrollTable.ColTitleList.List.Position.Offset) / gtx.Dp(s.CellWidth)
|
||||
dy := (int(e.Position.Y) + s.ScrollTable.RowTitleList.List.Position.Offset) / gtx.Dp(s.CellHeight)
|
||||
x := dx + s.ScrollTable.ColTitleList.List.Position.First
|
||||
y := dy + s.ScrollTable.RowTitleList.List.Position.First
|
||||
s.ScrollTable.Table.SetCursor(
|
||||
tracker.Point{X: x, Y: y},
|
||||
)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.ScrollTable.Table.SetCursor2(s.ScrollTable.Table.Cursor())
|
||||
}
|
||||
s.ScrollTable.cursorMoved = true
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
s.ScrollTable.command(gtx, e)
|
||||
}
|
||||
case transfer.DataEvent:
|
||||
if b, err := io.ReadAll(e.Open()); err == nil {
|
||||
s.ScrollTable.Table.Paste(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.Filter{Focus: s.ScrollTable.RowTitleList, Name: "→"},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.Filter{Focus: s.ScrollTable.ColTitleList, Name: "↓"},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) layoutTable(gtx C, p image.Point) {
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
|
||||
|
||||
if s.ScrollTable.requestFocus {
|
||||
s.ScrollTable.requestFocus = false
|
||||
gtx.Execute(key.FocusCmd{Tag: s.ScrollTable})
|
||||
}
|
||||
cellWidth := gtx.Dp(s.CellWidth)
|
||||
cellHeight := gtx.Dp(s.CellHeight)
|
||||
|
||||
gtx.Constraints = layout.Exact(image.Pt(cellWidth, cellHeight))
|
||||
|
||||
colP := s.ColTitleStyle.dragList.List.Position
|
||||
rowP := s.RowTitleStyle.dragList.List.Position
|
||||
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
||||
for x := 0; x < colP.Count; x++ {
|
||||
for y := 0; y < rowP.Count; y++ {
|
||||
o := op.Offset(image.Pt(cellWidth*x, cellHeight*y)).Push(gtx.Ops)
|
||||
s.element(gtx, x+colP.First, y+rowP.First)
|
||||
o.Pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutRowTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(0, p.Y)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = p.X
|
||||
gtx.Constraints.Max.Y -= p.Y
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
s.RowTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutColTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(p.X, 0)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.Y = p.Y
|
||||
gtx.Constraints.Max.X -= p.X
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
s.ColTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTable) command(gtx C, e key.Event) {
|
||||
stepX := 1
|
||||
stepY := 1
|
||||
if e.Modifiers.Contain(key.ModAlt) {
|
||||
stepX = intMax(s.ColTitleList.List.Position.Count-3, 8)
|
||||
stepY = intMax(s.RowTitleList.List.Position.Count-3, 8)
|
||||
} else if e.Modifiers.Contain(key.ModCtrl) {
|
||||
stepX = 1e6
|
||||
stepY = 1e6
|
||||
}
|
||||
switch e.Name {
|
||||
case "X", "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, ok := s.Table.Copy()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
||||
if e.Name == "X" {
|
||||
s.Table.Clear()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
gtx.Execute(clipboard.ReadCmd{Tag: s})
|
||||
}
|
||||
return
|
||||
case key.NameDeleteBackward, key.NameDeleteForward:
|
||||
s.Table.Clear()
|
||||
return
|
||||
case key.NameUpArrow:
|
||||
if !s.Table.MoveCursor(0, -stepY) && stepY == 1 {
|
||||
s.ColTitleList.Focus()
|
||||
}
|
||||
case key.NameDownArrow:
|
||||
s.Table.MoveCursor(0, stepY)
|
||||
case key.NameLeftArrow:
|
||||
if !s.Table.MoveCursor(-stepX, 0) && stepX == 1 {
|
||||
s.RowTitleList.Focus()
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
s.Table.MoveCursor(stepX, 0)
|
||||
case key.NamePageUp:
|
||||
s.Table.MoveCursor(0, -intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NamePageDown:
|
||||
s.Table.MoveCursor(0, intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NameHome:
|
||||
s.Table.SetCursorX(0)
|
||||
case key.NameEnd:
|
||||
s.Table.SetCursorX(s.Table.Width() - 1)
|
||||
case "+":
|
||||
s.Table.Add(1)
|
||||
return
|
||||
case "-":
|
||||
s.Table.Add(-1)
|
||||
return
|
||||
}
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.Table.SetCursor2(s.Table.Cursor())
|
||||
}
|
||||
s.ColTitleList.EnsureVisible(s.Table.Cursor().X)
|
||||
s.RowTitleList.EnsureVisible(s.Table.Cursor().Y)
|
||||
s.cursorMoved = true
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"image"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@ -77,19 +78,22 @@ func (s *ScrollBar) Layout(gtx C, width unit.Dp, numItems int, pos *layout.Posit
|
||||
rect := image.Rect(0, gtx.Constraints.Min.Y-scrWidth, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
||||
area = clip.Rect(rect).Push(gtx.Ops)
|
||||
}
|
||||
pointer.InputOp{Tag: &s.dragStart,
|
||||
Types: pointer.Drag | pointer.Press | pointer.Cancel | pointer.Release,
|
||||
Grab: s.dragging,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &s.dragStart)
|
||||
area.Pop()
|
||||
stack.Pop()
|
||||
|
||||
for _, ev := range gtx.Events(&s.dragStart) {
|
||||
for {
|
||||
ev, ok := gtx.Event(
|
||||
pointer.Filter{Target: &s.dragStart, Kinds: pointer.Press | pointer.Cancel | pointer.Release | pointer.Drag},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
if s.Axis == layout.Horizontal {
|
||||
s.dragStart = e.Position.X
|
||||
@ -114,17 +118,22 @@ func (s *ScrollBar) Layout(gtx C, width unit.Dp, numItems int, pos *layout.Posit
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
||||
area2 := clip.Rect(rect).Push(gtx.Ops)
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &s.tag,
|
||||
Types: pointer.Enter | pointer.Leave,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &s.tag)
|
||||
area2.Pop()
|
||||
|
||||
for _, ev := range gtx.Events(&s.tag) {
|
||||
for {
|
||||
ev, ok := gtx.Event(pointer.Filter{
|
||||
Target: &s.tag,
|
||||
Kinds: pointer.Enter | pointer.Leave,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Enter:
|
||||
s.hovering = true
|
||||
case pointer.Leave:
|
||||
|
||||
@ -2,222 +2,184 @@ package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type SongPanel struct {
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
SongLength *NumberInput
|
||||
|
||||
RewindBtn *ActionClickable
|
||||
PlayingBtn *BoolClickable
|
||||
RecordBtn *BoolClickable
|
||||
NoteTracking *BoolClickable
|
||||
PanicBtn *BoolClickable
|
||||
LoopBtn *BoolClickable
|
||||
|
||||
// File menu items
|
||||
fileMenuItems []MenuItem
|
||||
NewSong tracker.Action
|
||||
OpenSongFile tracker.Action
|
||||
SaveSongFile tracker.Action
|
||||
SaveSongAsFile tracker.Action
|
||||
ExportWav tracker.Action
|
||||
Quit tracker.Action
|
||||
|
||||
// Edit menu items
|
||||
editMenuItems []MenuItem
|
||||
}
|
||||
|
||||
func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||
ret := &SongPanel{
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
Menus: make([]Menu, 2),
|
||||
BPM: NewNumberInput(model.BPM().Int()),
|
||||
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),
|
||||
RowsPerBeat: NewNumberInput(model.RowsPerBeat().Int()),
|
||||
Step: NewNumberInput(model.Step().Int()),
|
||||
SongLength: NewNumberInput(model.SongLength().Int()),
|
||||
PanicBtn: NewBoolClickable(model.Panic().Bool()),
|
||||
LoopBtn: NewBoolClickable(model.LoopToggle().Bool()),
|
||||
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
||||
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
||||
PlayingBtn: NewBoolClickable(model.Playing().Bool()),
|
||||
RewindBtn: NewActionClickable(model.Rewind()),
|
||||
}
|
||||
ret.fileMenuItems = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O", Doer: model.OpenSong()},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S", Doer: model.SaveSong()},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As...", Doer: model.SaveSongAs()},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", Doer: model.Export()},
|
||||
}
|
||||
if canQuit {
|
||||
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", Doer: model.Quit()})
|
||||
}
|
||||
ret.editMenuItems = []MenuItem{
|
||||
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Doer: model.Undo()},
|
||||
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Doer: model.Redo()},
|
||||
{IconBytes: icons.ImageCrop, Text: "Remove unused data", Doer: model.RemoveUnused()},
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
const shortcutKey = "Ctrl+"
|
||||
|
||||
var fileMenuItems []MenuItem = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav..."},
|
||||
}
|
||||
|
||||
func init() {
|
||||
if CAN_QUIT {
|
||||
fileMenuItems = append(fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongPanel(gtx C) D {
|
||||
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenuBar),
|
||||
layout.Rigid(t.layoutSongOptions),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return s.layoutMenuBar(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return s.layoutSongOptions(gtx, t)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
|
||||
for clickable.Clicked() {
|
||||
menu.Visible = true
|
||||
}
|
||||
m := t.PopupMenu(menu)
|
||||
return func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
titleBtn := material.Button(t.Theme, clickable, title)
|
||||
titleBtn.Color = white
|
||||
titleBtn.Background = transparent
|
||||
titleBtn.CornerRadius = unit.Dp(0)
|
||||
dims := titleBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.X = gtx.Dp(width)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(1000))
|
||||
m.Layout(gtx, items...)
|
||||
return dims
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
|
||||
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.NewSong(false)
|
||||
case 1:
|
||||
t.OpenSongFile(false)
|
||||
case 2:
|
||||
t.SaveSongFile()
|
||||
case 3:
|
||||
t.SaveSongAsFile()
|
||||
case 4:
|
||||
t.WaveTypeDialog.Visible = true
|
||||
case 5:
|
||||
t.Quit(false)
|
||||
}
|
||||
clickedItem, hasClicked = t.Menus[0].Clicked()
|
||||
}
|
||||
|
||||
for clickedItem, hasClicked := t.Menus[1].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.Undo()
|
||||
case 1:
|
||||
t.Redo()
|
||||
case 2:
|
||||
if contents, err := yaml.Marshal(t.Song()); err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case 3:
|
||||
clipboard.ReadOp{Tag: t}.Add(gtx.Ops)
|
||||
case 4:
|
||||
t.RemoveUnusedData()
|
||||
}
|
||||
clickedItem, hasClicked = t.Menus[1].Clicked()
|
||||
}
|
||||
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200),
|
||||
fileMenuItems...,
|
||||
)),
|
||||
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200),
|
||||
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},
|
||||
MenuItem{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Disabled: !t.CanRedo()},
|
||||
MenuItem{IconBytes: icons.ContentContentCopy, Text: "Copy", ShortcutText: shortcutKey + "C"},
|
||||
MenuItem{IconBytes: icons.ContentContentPaste, Text: "Paste", ShortcutText: shortcutKey + "V"},
|
||||
MenuItem{IconBytes: icons.ImageCrop, Text: "Remove unused data"},
|
||||
)),
|
||||
layout.Rigid(tr.layoutMenu(gtx, "File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)),
|
||||
layout.Rigid(tr.layoutMenu(gtx, "Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
|
||||
paint.FillShape(gtx.Ops, songSurfaceColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
|
||||
var panicBtnStyle material.ButtonStyle
|
||||
if !t.Panic() {
|
||||
panicBtnStyle = LowEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
} else {
|
||||
panicBtnStyle = HighEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
}
|
||||
|
||||
for t.PanicBtn.Clicked() {
|
||||
t.SetPanic(!t.Panic())
|
||||
}
|
||||
|
||||
var recordBtnStyle material.ButtonStyle
|
||||
if !t.Recording() {
|
||||
recordBtnStyle = LowEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
} else {
|
||||
recordBtnStyle = HighEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
}
|
||||
|
||||
for t.RecordBtn.Clicked() {
|
||||
t.SetRecording(!t.Recording())
|
||||
}
|
||||
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)")
|
||||
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)")
|
||||
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)")
|
||||
recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
|
||||
noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)")
|
||||
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)")
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("LEN:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("LEN:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.SongLength.Value = t.Song().Score.Length
|
||||
numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32, "Song length")
|
||||
numStyle := NumericUpDown(tr.Theme, t.SongLength, "Song length")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetSongLength(t.SongLength.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("BPM:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("BPM:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.BPM.Value = t.Song().BPM
|
||||
numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999, "Beats per minute")
|
||||
numStyle := NumericUpDown(tr.Theme, t.BPM, "Beats per minute")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetBPM(t.BPM.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPP:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("RPP:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerPattern.Value = t.Song().Score.RowsPerPattern
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerPattern, 1, 255, "Rows per pattern")
|
||||
numStyle := NumericUpDown(tr.Theme, t.RowsPerPattern, "Rows per pattern")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetRowsPerPattern(t.RowsPerPattern.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPB:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("RPB:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerBeat.Value = t.Song().RowsPerBeat
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerBeat, 1, 32, "Rows per beat")
|
||||
numStyle := NumericUpDown(tr.Theme, t.RowsPerBeat, "Rows per beat")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetRowsPerBeat(t.RowsPerBeat.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("STP:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("STP:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
numStyle := NumericUpDown(t.Theme, t.Step, 0, 8, "Cursor step")
|
||||
numStyle := NumericUpDown(tr.Theme, t.Step, "Cursor step")
|
||||
numStyle.UnitsPerStep = unit.Dp(20)
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(VuMeter{AverageVolume: tr.Model.AverageVolume(), PeakVolume: tr.Model.PeakVolume(), Range: 100}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return panicBtnStyle.Layout(gtx)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(rewindBtnStyle.Layout),
|
||||
layout.Rigid(playBtnStyle.Layout),
|
||||
layout.Rigid(recordBtnStyle.Layout),
|
||||
layout.Rigid(noteTrackBtnStyle.Layout),
|
||||
layout.Rigid(loopBtnStyle.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return recordBtnStyle.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(VuMeter{AverageVolume: t.lastAvgVolume, PeakVolume: t.lastPeakVolume, Range: 100}.Layout),
|
||||
layout.Rigid(panicBtnStyle.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package gioui
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@ -48,14 +49,21 @@ func (s *Split) Layout(gtx layout.Context, first, second layout.Widget) layout.D
|
||||
|
||||
{ // handle input
|
||||
// Avoid affecting the input tree with pointer events.
|
||||
|
||||
for _, ev := range gtx.Events(s) {
|
||||
for {
|
||||
ev, ok := gtx.Event(pointer.Filter{
|
||||
Target: s,
|
||||
Kinds: pointer.Press | pointer.Drag | pointer.Release,
|
||||
// TODO: there should be a grab; there was Grab: s.drag,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
if s.drag {
|
||||
break
|
||||
@ -123,10 +131,7 @@ func (s *Split) Layout(gtx layout.Context, first, second layout.Widget) layout.D
|
||||
barRect = image.Rect(0, firstSize, gtx.Constraints.Max.X, secondOffset)
|
||||
}
|
||||
area := clip.Rect(barRect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: s,
|
||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
||||
Grab: s.drag,
|
||||
}.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, s)
|
||||
area.Pop()
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
@ -39,10 +40,13 @@ func (s Surface) Layout(gtx C, widget layout.Widget) D {
|
||||
return s.Inset.Layout(gtx, widget)
|
||||
}
|
||||
if s.FitSize {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Stacked(fg),
|
||||
)
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := fg(gtx)
|
||||
call := macro.Stop()
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
bg(gtx)
|
||||
call.Add(gtx.Ops)
|
||||
return dims
|
||||
}
|
||||
gtxbg := gtx
|
||||
gtxbg.Constraints.Min = gtxbg.Constraints.Max
|
||||
|
||||
@ -44,6 +44,7 @@ var twoBeatHighlight = color.NRGBA{R: 31, G: 51, B: 53, A: 255}
|
||||
var patternPlayColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255}
|
||||
var patternTextColor = primaryColor
|
||||
var patternCellColor = color.NRGBA{R: 255, G: 255, B: 255, A: 3}
|
||||
var loopMarkerColor = color.NRGBA{R: 252, G: 186, B: 3, A: 255}
|
||||
|
||||
var instrumentHoverColor = color.NRGBA{R: 30, G: 31, B: 38, A: 255}
|
||||
var instrumentNameColor = color.NRGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
@ -63,7 +64,7 @@ var inactiveLightSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255}
|
||||
var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
|
||||
|
||||
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 8}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
|
||||
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
|
||||
|
||||
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
|
||||
|
||||
@ -1,500 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
const trackRowHeight = 16
|
||||
const trackColWidth = 54
|
||||
const patmarkWidth = 16
|
||||
|
||||
type TrackEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *TipClickable
|
||||
DeleteTrackBtn *TipClickable
|
||||
AddSemitoneBtn *widget.Clickable
|
||||
SubtractSemitoneBtn *widget.Clickable
|
||||
AddOctaveBtn *widget.Clickable
|
||||
SubtractOctaveBtn *widget.Clickable
|
||||
NoteOffBtn *widget.Clickable
|
||||
trackPointerTag bool
|
||||
trackJumpPointerTag bool
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func NewTrackEditor() *TrackEditor {
|
||||
return &TrackEditor{
|
||||
TrackVoices: new(NumberInput),
|
||||
NewTrackBtn: new(TipClickable),
|
||||
DeleteTrackBtn: new(TipClickable),
|
||||
AddSemitoneBtn: new(widget.Clickable),
|
||||
SubtractSemitoneBtn: new(widget.Clickable),
|
||||
AddOctaveBtn: new(widget.Clickable),
|
||||
SubtractOctaveBtn: new(widget.Clickable),
|
||||
NoteOffBtn: new(widget.Clickable),
|
||||
}
|
||||
}
|
||||
|
||||
func (te *TrackEditor) Focus() {
|
||||
te.requestFocus = true
|
||||
}
|
||||
|
||||
func (te *TrackEditor) Focused() bool {
|
||||
return te.focused || te.ChildFocused()
|
||||
}
|
||||
|
||||
func (te *TrackEditor) ChildFocused() bool {
|
||||
return te.AddOctaveBtn.Focused() || te.AddSemitoneBtn.Focused() || te.DeleteTrackBtn.Clickable.Focused() || te.NewTrackBtn.Clickable.Focused() || te.NoteOffBtn.Focused() || te.SubtractOctaveBtn.Focused() || te.SubtractSemitoneBtn.Focused() || te.SubtractSemitoneBtn.Focused() || te.SubtractSemitoneBtn.Focused()
|
||||
}
|
||||
|
||||
var trackerEditorKeys key.Set = "+|-|←|→|↑|↓|Ctrl-←|Ctrl-→|Ctrl-↑|Ctrl-↓|Shift-←|Shift-→|Shift-↑|Shift-↓|⏎|⇱|⇲|⌫|⌦|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|0|1|2|3|4|5|6|7|8|9|,|."
|
||||
|
||||
func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||
for _, e := range gtx.Events(te) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
te.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: te}.Add(gtx.Ops)
|
||||
}
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDeleteForward, key.NameDeleteBackward:
|
||||
t.DeleteSelection()
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.NameUpArrow, key.NameDownArrow:
|
||||
sign := -1
|
||||
if e.Name == key.NameDownArrow {
|
||||
sign = 1
|
||||
}
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row += t.Song().Score.RowsPerPattern * sign
|
||||
} else {
|
||||
if t.Step.Value > 0 {
|
||||
cursor.Row += t.Step.Value * sign
|
||||
} else {
|
||||
cursor.Row += sign
|
||||
}
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
//scrollToView(t.PatternOrderList, t.Cursor().Pattern, t.Song().Score.Length)
|
||||
case key.NameLeftArrow:
|
||||
cursor := t.Cursor()
|
||||
if !t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track--
|
||||
t.SetLowNibble(true)
|
||||
} else {
|
||||
t.SetLowNibble(false)
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
if t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor := t.Cursor()
|
||||
cursor.Track++
|
||||
t.SetCursor(cursor)
|
||||
t.SetLowNibble(false)
|
||||
} else {
|
||||
t.SetLowNibble(true)
|
||||
}
|
||||
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case "+":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
case "-":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
continue
|
||||
}
|
||||
step := false
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
|
||||
t.NumberPressed(byte(iv))
|
||||
step = true
|
||||
}
|
||||
} else {
|
||||
if e.Name == "A" || e.Name == "1" {
|
||||
t.SetNote(0)
|
||||
step = true
|
||||
} else {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := noteAsValue(t.OctaveNumberInput.Value, val)
|
||||
t.SetNote(n)
|
||||
step = true
|
||||
trk := t.Cursor().Track
|
||||
noteID := tracker.NoteIDTrack(trk, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step && !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.Release:
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if te.requestFocus || te.ChildFocused() {
|
||||
te.requestFocus = false
|
||||
key.FocusOp{Tag: te}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
rowMarkers := layout.Rigid(t.layoutRowMarkers)
|
||||
|
||||
for te.NewTrackBtn.Clickable.Clicked() {
|
||||
t.AddTrack(true)
|
||||
}
|
||||
|
||||
for te.DeleteTrackBtn.Clickable.Clicked() {
|
||||
t.DeleteTrack(false)
|
||||
}
|
||||
|
||||
//t.TrackHexCheckBoxes[i2].Value = t.TrackShowHex[i2]
|
||||
//cbStyle := material.CheckBox(t.Theme, t.TrackHexCheckBoxes[i2], "hex")
|
||||
//cbStyle.Color = white
|
||||
//cbStyle.IconColor = t.Theme.Fg
|
||||
|
||||
for te.AddSemitoneBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
|
||||
for te.SubtractSemitoneBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
|
||||
for te.NoteOffBtn.Clicked() {
|
||||
t.SetNote(0)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
|
||||
for te.AddOctaveBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(12)
|
||||
}
|
||||
|
||||
for te.SubtractOctaveBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
}
|
||||
|
||||
menu := func(gtx C) D {
|
||||
addSemitoneBtnStyle := LowEmphasisButton(t.Theme, te.AddSemitoneBtn, "+1")
|
||||
subtractSemitoneBtnStyle := LowEmphasisButton(t.Theme, te.SubtractSemitoneBtn, "-1")
|
||||
addOctaveBtnStyle := LowEmphasisButton(t.Theme, te.AddOctaveBtn, "+12")
|
||||
subtractOctaveBtnStyle := LowEmphasisButton(t.Theme, te.SubtractOctaveBtn, "-12")
|
||||
noteOffBtnStyle := LowEmphasisButton(t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := IconButton(t.Theme, te.DeleteTrackBtn, icons.ActionDelete, t.CanDeleteTrack(), "Delete track")
|
||||
newTrackBtnStyle := IconButton(t.Theme, te.NewTrackBtn, icons.ContentAdd, t.CanAddTrack(), "Add track")
|
||||
n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices
|
||||
te.TrackVoices.Value = n
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, 1, t.MaxTrackVoices(), "Number of voices for this track")
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
t.TrackHexCheckBox.Value = t.Song().Score.Tracks[t.Cursor().Track].Effect
|
||||
hexCheckBoxStyle := material.CheckBox(t.Theme, t.TrackHexCheckBox, "Hex")
|
||||
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(addOctaveBtnStyle.Layout),
|
||||
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
||||
layout.Rigid(noteOffBtnStyle.Layout),
|
||||
layout.Rigid(hexCheckBoxStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.TextShaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
t.Song().Score.Tracks[t.Cursor().Track].Effect = t.TrackHexCheckBox.Value // TODO: we should not modify the model, but how should this be done
|
||||
t.SetTrackVoices(te.TrackVoices.Value)
|
||||
return dims
|
||||
}
|
||||
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: te,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
key.InputOp{Tag: te, Keys: trackerEditorKeys}.Add(gtx.Ops)
|
||||
|
||||
dims := Surface{Gray: 24, Focus: te.Focused()}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return Surface{Gray: 37, Focus: te.Focused(), FitSize: true}.Layout(gtx, menu)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
rowMarkers,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return te.layoutTracks(gtx, t)
|
||||
}))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
area.Pop()
|
||||
return dims
|
||||
}
|
||||
|
||||
const baseNote = 24
|
||||
|
||||
var notes = []string{
|
||||
"C-",
|
||||
"C#",
|
||||
"D-",
|
||||
"D#",
|
||||
"E-",
|
||||
"F-",
|
||||
"F#",
|
||||
"G-",
|
||||
"G#",
|
||||
"A-",
|
||||
"A#",
|
||||
"B-",
|
||||
}
|
||||
|
||||
func noteStr(val byte) string {
|
||||
if val == 1 {
|
||||
return "..." // hold
|
||||
}
|
||||
if val == 0 {
|
||||
return "---" // release
|
||||
}
|
||||
oNote := mod(int(val-baseNote), 12)
|
||||
octave := (int(val) - oNote - baseNote) / 12
|
||||
if octave < 0 {
|
||||
return fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
||||
}
|
||||
if octave >= 10 {
|
||||
return fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
||||
}
|
||||
return fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
|
||||
func noteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
func (te *TrackEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
for _, ev := range gtx.Events(&te.trackJumpPointerTag) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press {
|
||||
te.Focus()
|
||||
track := int(e.Position.X) / trackColWidth
|
||||
row := int((e.Position.Y-float32(gtx.Constraints.Max.Y-trackRowHeight)/2)/trackRowHeight + float32(cursorSongRow))
|
||||
cursor := tracker.ScorePoint{Track: track, ScoreRow: tracker.ScoreRow{Row: row}}.Clamp(t.Song().Score)
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
cursorSongRow = cursor.Pattern*t.Song().Score.RowsPerPattern + cursor.Row
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &te.trackJumpPointerTag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
stack := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
curVoice := 0
|
||||
for _, trk := range t.Song().Score.Tracks {
|
||||
gtx := gtx
|
||||
instrName := "?"
|
||||
firstIndex, err := t.Song().Patch.InstrumentForVoice(curVoice)
|
||||
lastIndex, err2 := t.Song().Patch.InstrumentForVoice(curVoice + trk.NumVoices - 1)
|
||||
if err == nil && err2 == nil {
|
||||
switch diff := lastIndex - firstIndex; diff {
|
||||
case 0:
|
||||
instrName = t.Song().Patch[firstIndex].Name
|
||||
default:
|
||||
n1 := t.Song().Patch[firstIndex].Name
|
||||
n2 := t.Song().Patch[firstIndex+1].Name
|
||||
if len(n1) > 0 {
|
||||
n1 = string(n1[0])
|
||||
} else {
|
||||
n1 = "?"
|
||||
}
|
||||
if len(n2) > 0 {
|
||||
n2 = string(n2[0])
|
||||
} else {
|
||||
n2 = "?"
|
||||
}
|
||||
if diff > 1 {
|
||||
instrName = n1 + "/" + n2 + "..."
|
||||
} else {
|
||||
instrName = n1 + "/" + n2
|
||||
}
|
||||
}
|
||||
if len(instrName) > 7 {
|
||||
instrName = instrName[:7]
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Max.X = trackColWidth
|
||||
LabelStyle{Alignment: layout.N, Text: instrName, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.TextShaper}.Layout(gtx)
|
||||
op.Offset(image.Point{trackColWidth, 0}).Add(gtx.Ops)
|
||||
curVoice += trk.NumVoices
|
||||
}
|
||||
stack.Pop()
|
||||
op.Offset(image.Point{0, (gtx.Constraints.Max.Y - trackRowHeight) / 2}).Add(gtx.Ops)
|
||||
op.Offset(image.Point{0, int((-1 * trackRowHeight) * (cursorSongRow))}).Add(gtx.Ops)
|
||||
if te.Focused() || t.OrderEditor.Focused() {
|
||||
x1, y1 := t.Cursor().Track, t.Cursor().Pattern
|
||||
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
paint.FillShape(gtx.Ops, inactiveSelectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
}
|
||||
if te.Focused() {
|
||||
x1, y1 := t.Cursor().Track, t.Cursor().Pattern*t.Song().Score.RowsPerPattern+t.Cursor().Row
|
||||
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern*t.Song().Score.RowsPerPattern+t.SelectionCorner().Row
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight
|
||||
paint.FillShape(gtx.Ops, selectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
cx := t.Cursor().Track * trackColWidth
|
||||
cy := (t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row) * trackRowHeight
|
||||
cw := trackColWidth
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
cw /= 2
|
||||
if t.LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+cw, cy+trackRowHeight)}.Op())
|
||||
}
|
||||
delta := (gtx.Constraints.Max.Y/2 + trackRowHeight - 1) / trackRowHeight
|
||||
firstRow := cursorSongRow - delta
|
||||
lastRow := cursorSongRow + delta
|
||||
if firstRow < 0 {
|
||||
firstRow = 0
|
||||
}
|
||||
if l := t.Song().Score.LengthInRows(); lastRow >= l {
|
||||
lastRow = l - 1
|
||||
}
|
||||
op.Offset(image.Point{0, trackRowHeight * firstRow}).Add(gtx.Ops)
|
||||
for trkIndex, trk := range t.Song().Score.Tracks {
|
||||
stack := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
for row := firstRow; row <= lastRow; row++ {
|
||||
pat := row / t.Song().Score.RowsPerPattern
|
||||
patRow := row % t.Song().Score.RowsPerPattern
|
||||
s := trk.Order.Get(pat)
|
||||
if s < 0 {
|
||||
op.Offset(image.Point{0, trackRowHeight}).Add(gtx.Ops)
|
||||
continue
|
||||
}
|
||||
if s >= 0 && patRow == 0 {
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
||||
}
|
||||
if s >= 0 && patRow == 1 && t.IsPatternUnique(trkIndex, s) {
|
||||
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
||||
}
|
||||
op.Offset(image.Point{patmarkWidth, 0}).Add(gtx.Ops)
|
||||
if te.Focused() && t.Cursor().Row == patRow && t.Cursor().Pattern == pat {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
var c byte = 1
|
||||
if s >= 0 && s < len(trk.Patterns) {
|
||||
c = trk.Patterns[s].Get(patRow)
|
||||
}
|
||||
if trk.Effect {
|
||||
var text string
|
||||
switch c {
|
||||
case 0:
|
||||
text = "--"
|
||||
case 1:
|
||||
text = ".."
|
||||
default:
|
||||
text = fmt.Sprintf("%02x", c)
|
||||
}
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(text), op.CallOp{})
|
||||
} else {
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, noteStr(c), op.CallOp{})
|
||||
}
|
||||
op.Offset(image.Point{-patmarkWidth, trackRowHeight}).Add(gtx.Ops)
|
||||
}
|
||||
stack.Pop()
|
||||
op.Offset(image.Point{trackColWidth, 0}).Add(gtx.Ops)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
@ -1,24 +1,63 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/explorer"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var canQuit = true // set to false in init() if plugin tag is enabled
|
||||
|
||||
type (
|
||||
Tracker struct {
|
||||
Theme *material.Theme
|
||||
OctaveNumberInput *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[key.Name]tracker.NoteID
|
||||
PopupAlert *PopupAlert
|
||||
|
||||
SaveChangesDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *NoteEditor
|
||||
Explorer *explorer.Explorer
|
||||
Exploring bool
|
||||
SongPanel *SongPanel
|
||||
|
||||
filePathString tracker.String
|
||||
|
||||
quitWG sync.WaitGroup
|
||||
execChan chan func()
|
||||
|
||||
*tracker.Model
|
||||
}
|
||||
|
||||
C = layout.Context
|
||||
D = layout.Dimensions
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,140 +66,34 @@ const (
|
||||
ConfirmNew
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
Theme *material.Theme
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
OctaveNumberInput *NumberInput
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
SongLength *NumberInput
|
||||
PanicBtn *widget.Clickable
|
||||
RecordBtn *widget.Clickable
|
||||
AddUnitBtn *widget.Clickable
|
||||
TrackHexCheckBox *widget.Bool
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[string]tracker.NoteID
|
||||
Alert Alert
|
||||
ConfirmSongDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
ConfirmSongActionType int
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *TrackEditor
|
||||
Explorer *explorer.Explorer
|
||||
|
||||
TextShaper *text.Shaper
|
||||
|
||||
lastAvgVolume tracker.Volume
|
||||
lastPeakVolume tracker.Volume
|
||||
|
||||
wavFilePath string
|
||||
quitChannel chan struct{}
|
||||
quitWG sync.WaitGroup
|
||||
errorChannel chan error
|
||||
quitted bool
|
||||
unmarshalRecoveryChannel chan []byte
|
||||
marshalRecoveryChannel chan (chan []byte)
|
||||
synther sointu.Synther
|
||||
|
||||
*trackerModel
|
||||
}
|
||||
|
||||
type trackerModel = tracker.Model
|
||||
|
||||
func (t *Tracker) UnmarshalContent(bytes []byte) error {
|
||||
var units []sointu.Unit
|
||||
if errJSON := json.Unmarshal(bytes, &units); errJSON == nil {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
t.PasteUnits(units)
|
||||
// TODO: this is a bit hacky, but works for now. How to change the selection to the pasted units more elegantly?
|
||||
t.InstrumentEditor.unitDragList.SelectedItem = t.UnitIndex()
|
||||
t.InstrumentEditor.unitDragList.SelectedItem2 = t.UnitIndex() + len(units) - 1
|
||||
return nil
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &units); errYaml == nil {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
t.PasteUnits(units)
|
||||
t.InstrumentEditor.unitDragList.SelectedItem = t.UnitIndex()
|
||||
t.InstrumentEditor.unitDragList.SelectedItem2 = t.UnitIndex() + len(units) - 1
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
if song.BPM > 0 {
|
||||
t.SetSong(song)
|
||||
return nil
|
||||
}
|
||||
return errors.New("was able to unmarshal a song, but the bpm was 0")
|
||||
}
|
||||
|
||||
func NewTracker(model *tracker.Model, synther sointu.Synther) *Tracker {
|
||||
func NewTracker(model *tracker.Model) *Tracker {
|
||||
t := &Tracker{
|
||||
Theme: material.NewTheme(),
|
||||
BPM: new(NumberInput),
|
||||
OctaveNumberInput: &NumberInput{Value: 4},
|
||||
SongLength: new(NumberInput),
|
||||
RowsPerPattern: new(NumberInput),
|
||||
RowsPerBeat: new(NumberInput),
|
||||
Step: &NumberInput{Value: 1},
|
||||
InstrumentVoices: new(NumberInput),
|
||||
OctaveNumberInput: NewNumberInput(model.Octave().Int()),
|
||||
InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()),
|
||||
|
||||
PanicBtn: new(widget.Clickable),
|
||||
RecordBtn: new(widget.Clickable),
|
||||
TrackHexCheckBox: new(widget.Bool),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
quitChannel: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
|
||||
|
||||
TopHorizontalSplit: &Split{Ratio: -.6},
|
||||
TopHorizontalSplit: &Split{Ratio: -.5},
|
||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||
|
||||
KeyPlaying: make(map[string]tracker.NoteID),
|
||||
ConfirmSongDialog: new(Dialog),
|
||||
WaveTypeDialog: new(Dialog),
|
||||
InstrumentEditor: NewInstrumentEditor(),
|
||||
OrderEditor: NewOrderEditor(),
|
||||
TrackEditor: NewTrackEditor(),
|
||||
KeyPlaying: make(map[key.Name]tracker.NoteID),
|
||||
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
|
||||
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
|
||||
InstrumentEditor: NewInstrumentEditor(model),
|
||||
OrderEditor: NewOrderEditor(model),
|
||||
TrackEditor: NewNoteEditor(model),
|
||||
SongPanel: NewSongPanel(model),
|
||||
|
||||
errorChannel: make(chan error, 32),
|
||||
synther: synther,
|
||||
trackerModel: model,
|
||||
Model: model,
|
||||
|
||||
marshalRecoveryChannel: make(chan (chan []byte)),
|
||||
unmarshalRecoveryChannel: make(chan []byte),
|
||||
filePathString: model.FilePath().String(),
|
||||
execChan: make(chan func(), 1024),
|
||||
}
|
||||
t.TextShaper = text.NewShaper(text.WithCollection(fontCollection))
|
||||
t.Alert.shaper = t.TextShaper
|
||||
t.Theme.Shaper = text.NewShaper(text.WithCollection(fontCollection))
|
||||
t.PopupAlert = NewPopupAlert(model.Alerts(), t.Theme.Shaper)
|
||||
t.Theme.Palette.Fg = primaryColor
|
||||
t.Theme.Palette.ContrastFg = black
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
t.quitWG.Add(1)
|
||||
return t
|
||||
}
|
||||
@ -171,19 +104,18 @@ func (t *Tracker) Main() {
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t.InstrumentEditor.Focus()
|
||||
recoveryTicker := time.NewTicker(time.Second * 30)
|
||||
t.Explorer = explorer.NewExplorer(w)
|
||||
// Make a channel to read window events from.
|
||||
events := make(chan event.Event)
|
||||
// Make a channel to signal the end of processing a window event.
|
||||
acks := make(chan struct{})
|
||||
go eventLoop(w, events, acks)
|
||||
var ops op.Ops
|
||||
mainloop:
|
||||
for {
|
||||
if pos, playing := t.PlayPosition(), t.Playing(); t.NoteTracking() && playing {
|
||||
cursor := t.Cursor()
|
||||
cursor.ScoreRow = pos
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
}
|
||||
if titleFooter != t.FilePath() {
|
||||
titleFooter = t.FilePath()
|
||||
if titleFooter != t.filePathString.Value() {
|
||||
titleFooter = t.filePathString.Value()
|
||||
if titleFooter != "" {
|
||||
w.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", titleFooter)))
|
||||
} else {
|
||||
@ -191,72 +123,188 @@ mainloop:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-t.quitChannel:
|
||||
recoveryTicker.Stop()
|
||||
break mainloop
|
||||
case e := <-t.errorChannel:
|
||||
t.Alert.Update(e.Error(), Error, time.Second*5)
|
||||
w.Invalidate()
|
||||
case e := <-t.PlayerMessages:
|
||||
if err, ok := e.Inner.(tracker.PlayerCrashMessage); ok {
|
||||
t.Alert.Update(err.Error(), Error, time.Second*3)
|
||||
}
|
||||
if err, ok := e.Inner.(tracker.PlayerVolumeErrorMessage); ok {
|
||||
t.Alert.Update(err.Error(), Warning, time.Second*3)
|
||||
}
|
||||
t.lastAvgVolume = e.AverageVolume
|
||||
t.lastPeakVolume = e.PeakVolume
|
||||
t.InstrumentEditor.voiceLevels = e.VoiceLevels
|
||||
t.ProcessPlayerMessage(e)
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
case e := <-events:
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
if !t.Quit(false) {
|
||||
case app.DestroyEvent:
|
||||
acks <- struct{}{}
|
||||
if canQuit {
|
||||
t.Quit().Do()
|
||||
}
|
||||
if !t.Quitted() {
|
||||
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
|
||||
w = app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t.Explorer = explorer.NewExplorer(w)
|
||||
go eventLoop(w, events, acks)
|
||||
}
|
||||
case app.FrameEvent:
|
||||
gtx := app.NewContext(&ops, e)
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && t.SongPanel.NoteTracking.Bool.Value() {
|
||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(t.PlaySongRow())
|
||||
}
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
t.Layout(gtx, w)
|
||||
e.Frame(gtx.Ops)
|
||||
acks <- struct{}{}
|
||||
default:
|
||||
acks <- struct{}{}
|
||||
}
|
||||
case <-recoveryTicker.C:
|
||||
t.SaveRecovery()
|
||||
case retChn := <-t.marshalRecoveryChannel:
|
||||
retChn <- t.MarshalRecovery()
|
||||
case bytes := <-t.unmarshalRecoveryChannel:
|
||||
t.UnmarshalRecovery(bytes)
|
||||
case f := <-t.execChan:
|
||||
f()
|
||||
}
|
||||
if t.Quitted() {
|
||||
break
|
||||
}
|
||||
}
|
||||
recoveryTicker.Stop()
|
||||
w.Perform(system.ActionClose)
|
||||
t.SaveRecovery()
|
||||
t.quitWG.Done()
|
||||
}
|
||||
|
||||
// thread safe, executed in the GUI thread
|
||||
func (t *Tracker) SafeMarshalRecovery() []byte {
|
||||
retChn := make(chan []byte)
|
||||
t.marshalRecoveryChannel <- retChn
|
||||
return <-retChn
|
||||
}
|
||||
|
||||
// thread safe, executed in the GUI thread
|
||||
func (t *Tracker) SafeUnmarshalRecovery(data []byte) {
|
||||
t.unmarshalRecoveryChannel <- data
|
||||
}
|
||||
|
||||
func (t *Tracker) sendQuit() {
|
||||
select {
|
||||
case t.quitChannel <- struct{}{}:
|
||||
default:
|
||||
func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) {
|
||||
// Iterate window events, sending each to the old event loop and waiting for
|
||||
// a signal that processing is complete before iterating again.
|
||||
for {
|
||||
ev := w.NextEvent()
|
||||
events <- ev
|
||||
<-acks
|
||||
if _, ok := ev.(app.DestroyEvent); ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) Exec() chan<- func() {
|
||||
return t.execChan
|
||||
}
|
||||
|
||||
func (t *Tracker) WaitQuitted() {
|
||||
t.quitWG.Wait()
|
||||
}
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.PopupAlert.Layout(gtx)
|
||||
t.showDialog(gtx)
|
||||
// this is the top level input handler for the whole app
|
||||
// it handles all the global key events and clipboard events
|
||||
// we need to tell gio that we handle tabs too; otherwise
|
||||
// it will steal them for focus switching
|
||||
for {
|
||||
ev, ok := gtx.Event(
|
||||
key.Filter{Name: "", Optional: key.ModAlt | key.ModCommand | key.ModShift | key.ModShortcut | key.ModSuper},
|
||||
key.Filter{Name: key.NameTab, Optional: key.ModShift},
|
||||
transfer.TargetFilter{Target: t, Type: "application/text"},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch e := ev.(type) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx)
|
||||
case transfer.DataEvent:
|
||||
t.ReadSong(e.Open())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *Tracker) showDialog(gtx C) {
|
||||
if t.Exploring {
|
||||
return
|
||||
}
|
||||
switch t.Dialog() {
|
||||
case tracker.NewSongChanges, tracker.OpenSongChanges, tracker.QuitChanges:
|
||||
dstyle := ConfirmDialog(gtx, t.Theme, t.SaveChangesDialog, "Save changes to song?", "Your changes will be lost if you don't save them.")
|
||||
dstyle.OkStyle.Text = "Save"
|
||||
dstyle.AltStyle.Text = "Don't save"
|
||||
dstyle.Layout(gtx)
|
||||
case tracker.Export:
|
||||
dstyle := ConfirmDialog(gtx, t.Theme, t.WaveTypeDialog, "", "Export .wav in int16 or float32 sample format?")
|
||||
dstyle.OkStyle.Text = "Int16"
|
||||
dstyle.AltStyle.Text = "Float32"
|
||||
dstyle.Layout(gtx)
|
||||
case tracker.OpenSongOpenExplorer:
|
||||
t.explorerChooseFile(t.ReadSong, ".yml", ".json")
|
||||
case tracker.NewSongSaveExplorer, tracker.OpenSongSaveExplorer, tracker.QuitSaveExplorer, tracker.SaveAsExplorer:
|
||||
filename := t.filePathString.Value()
|
||||
if filename == "" {
|
||||
filename = "song.yml"
|
||||
}
|
||||
t.explorerCreateFile(t.WriteSong, filename)
|
||||
case tracker.ExportFloatExplorer, tracker.ExportInt16Explorer:
|
||||
filename := "song.wav"
|
||||
if p := t.filePathString.Value(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
t.explorerCreateFile(func(wc io.WriteCloser) {
|
||||
t.WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer, t.execChan)
|
||||
}, filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) explorerChooseFile(success func(io.ReadCloser), extensions ...string) {
|
||||
t.Exploring = true
|
||||
go func() {
|
||||
file, err := t.Explorer.ChooseFile(extensions...)
|
||||
t.Exec() <- func() {
|
||||
t.Exploring = false
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tracker) explorerCreateFile(success func(io.WriteCloser), filename string) {
|
||||
t.Exploring = true
|
||||
go func() {
|
||||
file, err := t.Explorer.CreateFile(filename)
|
||||
t.Exec() <- func() {
|
||||
t.Exploring = false
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.OrderEditor.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.TrackEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
return t.TopHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.SongPanel.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.InstrumentEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
//go:build !plugin
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = true
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmQuit
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return false
|
||||
}
|
||||
t.sendQuit()
|
||||
return true
|
||||
}
|
||||
@ -2,11 +2,6 @@
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = false
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if forced {
|
||||
t.sendQuit()
|
||||
}
|
||||
return forced
|
||||
func init() {
|
||||
canQuit = false
|
||||
}
|
||||
|
||||
341
tracker/gioui/unit_editor.go
Normal file
341
tracker/gioui/unit_editor.go
Normal file
@ -0,0 +1,341 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type UnitEditor struct {
|
||||
sliderList *DragList
|
||||
searchList *DragList
|
||||
Parameters []*ParameterWidget
|
||||
DeleteUnitBtn *ActionClickable
|
||||
CopyUnitBtn *TipClickable
|
||||
ClearUnitBtn *ActionClickable
|
||||
DisableUnitBtn *BoolClickable
|
||||
SelectTypeBtn *widget.Clickable
|
||||
caser cases.Caser
|
||||
}
|
||||
|
||||
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||
ret := &UnitEditor{
|
||||
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
|
||||
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
|
||||
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
|
||||
CopyUnitBtn: new(TipClickable),
|
||||
SelectTypeBtn: new(widget.Clickable),
|
||||
sliderList: NewDragList(m.Params().List(), layout.Vertical),
|
||||
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
|
||||
}
|
||||
ret.caser = cases.Title(language.English)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.Filter{Focus: pe.sliderList, Name: key.NameLeftArrow, Optional: key.ModShift},
|
||||
key.Filter{Focus: pe.sliderList, Name: key.NameRightArrow, Optional: key.ModShift},
|
||||
key.Filter{Focus: pe.sliderList, Name: key.NameEscape},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
pe.command(e, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
editorFunc := pe.layoutSliders
|
||||
|
||||
if t.UnitSearching().Value() || pe.sliderList.TrackerList.Count() == 0 {
|
||||
editorFunc = pe.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return editorFunc(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return pe.layoutFooter(gtx, t)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D {
|
||||
numItems := pe.sliderList.TrackerList.Count()
|
||||
|
||||
for len(pe.Parameters) < numItems {
|
||||
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
|
||||
}
|
||||
|
||||
index := 0
|
||||
t.Model.Params().Iterate(func(param tracker.Parameter) {
|
||||
pe.Parameters[index].Parameter = param
|
||||
index++
|
||||
})
|
||||
|
||||
element := func(gtx C, index int) D {
|
||||
if index < 0 || index >= numItems {
|
||||
return D{}
|
||||
}
|
||||
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
|
||||
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
|
||||
dims := paramStyle.Layout(gtx)
|
||||
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
|
||||
}
|
||||
|
||||
fdl := FilledDragList(t.Theme, pe.sliderList, element, nil)
|
||||
dims := fdl.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
|
||||
for pe.CopyUnitBtn.Clickable.Clicked(gtx) {
|
||||
if contents, ok := t.Units().List().CopyElements(); ok {
|
||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
||||
t.Alerts().Add("Unit copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, "Copy unit (Ctrl+C)")
|
||||
deleteUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
|
||||
disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, "Disable unit (Ctrl-D)", "Enable unit (Ctrl-D)")
|
||||
text := t.Units().SelectedType()
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = pe.caser.String(text)
|
||||
}
|
||||
hintText := Label(text, white, t.Theme.Shaper)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(copyUnitBtnStyle.Layout),
|
||||
layout.Rigid(disableUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Units().SelectedType() != "" {
|
||||
clearUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D {
|
||||
var names [256]string
|
||||
index := 0
|
||||
t.Model.SearchResults().Iterate(func(item string) (ok bool) {
|
||||
names[index] = item
|
||||
index++
|
||||
return index <= 256
|
||||
})
|
||||
element := func(gtx C, i int) D {
|
||||
w := LabelStyle{Text: names[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
if i == pe.searchList.TrackerList.Selected() {
|
||||
for pe.SelectTypeBtn.Clicked(gtx) {
|
||||
t.Units().SetSelectedType(names[i])
|
||||
}
|
||||
return pe.SelectTypeBtn.Layout(gtx, w.Layout)
|
||||
}
|
||||
return w.Layout(gtx)
|
||||
}
|
||||
fdl := FilledDragList(t.Theme, pe.searchList, element, nil)
|
||||
dims := fdl.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) command(e key.Event, t *Tracker) {
|
||||
params := (*tracker.Params)(t.Model)
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameLeftArrow:
|
||||
sel := params.SelectedItem()
|
||||
if sel == nil {
|
||||
return
|
||||
}
|
||||
i := (&tracker.Int{IntData: sel})
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
i.Set(i.Value() - sel.LargeStep())
|
||||
} else {
|
||||
i.Set(i.Value() - 1)
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
sel := params.SelectedItem()
|
||||
if sel == nil {
|
||||
return
|
||||
}
|
||||
i := (&tracker.Int{IntData: sel})
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
i.Set(i.Value() + sel.LargeStep())
|
||||
} else {
|
||||
i.Set(i.Value() + 1)
|
||||
}
|
||||
case key.NameEscape:
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
Parameter tracker.Parameter
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
w *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Theme: th,
|
||||
w: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.w.Parameter.Type() {
|
||||
case tracker.IntegerParameter:
|
||||
for p.Focus {
|
||||
e, ok := gtx.Event(pointer.Filter{
|
||||
Target: &p.w.floatWidget,
|
||||
Kinds: pointer.Scroll,
|
||||
ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)},
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
tracker.Int{IntData: p.w.Parameter}.Add(-int(delta))
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
ra := p.w.Parameter.Range()
|
||||
if !p.w.floatWidget.Dragging() {
|
||||
p.w.floatWidget.Value = (float32(p.w.Parameter.Value()) - float32(ra.Min)) / float32(ra.Max-ra.Min)
|
||||
}
|
||||
sliderStyle := material.Slider(p.Theme, &p.w.floatWidget)
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
r := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
area := clip.Rect(r).Push(gtx.Ops)
|
||||
if p.Focus {
|
||||
event.Op(gtx.Ops, &p.w.floatWidget)
|
||||
}
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
area.Pop()
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(int(p.w.floatWidget.Value*float32(ra.Max-ra.Min) + float32(ra.Min) + 0.5))
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
ra := p.w.Parameter.Range()
|
||||
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.w.boolWidget, "Toggle boolean parameter")
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.w.boolWidget.Value {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(ra.Max)
|
||||
} else {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(ra.Min)
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
||||
for i := range instrItems {
|
||||
i := i
|
||||
name, _, _ := p.tracker.Instruments().Item(i)
|
||||
instrItems[i].Text = name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
instrItems[i].Doer = tracker.Allow(func() {
|
||||
if id, ok := p.tracker.Instruments().FirstID(i); ok {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.FindUnit(p.w.Parameter.Value())
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Instrument(targetI)
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for j, unit := range units {
|
||||
id := unit.ID
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
unitItems[j].Doer = tracker.Allow(func() {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(gtx, instrName, &p.w.instrBtn, &p.w.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(gtx, unitName, &p.w.unitBtn, &p.w.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.w.Parameter.Type() != tracker.IDParameter {
|
||||
return Label(p.w.Parameter.Hint(), white, p.tracker.Theme.Shaper)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
||||
191
tracker/int.go
Normal file
191
tracker/int.go
Normal file
@ -0,0 +1,191 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
Int struct {
|
||||
IntData
|
||||
}
|
||||
|
||||
IntData interface {
|
||||
Value() int
|
||||
Range() intRange
|
||||
|
||||
setValue(int)
|
||||
change(kind string) func()
|
||||
}
|
||||
|
||||
intRange struct {
|
||||
Min, Max int
|
||||
}
|
||||
|
||||
InstrumentVoices Model
|
||||
TrackVoices Model
|
||||
SongLength Model
|
||||
BPM Model
|
||||
RowsPerPattern Model
|
||||
RowsPerBeat Model
|
||||
Step Model
|
||||
Octave Model
|
||||
)
|
||||
|
||||
func (v Int) Add(delta int) (ok bool) {
|
||||
r := v.Range()
|
||||
value := r.Clamp(v.Value() + delta)
|
||||
if value == v.Value() || value < r.Min || value > r.Max {
|
||||
return false
|
||||
}
|
||||
defer v.change("Add")()
|
||||
v.setValue(value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v Int) Set(value int) (ok bool) {
|
||||
r := v.Range()
|
||||
value = v.Range().Clamp(value)
|
||||
if value == v.Value() || value < r.Min || value > r.Max {
|
||||
return false
|
||||
}
|
||||
defer v.change("Set")()
|
||||
v.setValue(value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r intRange) Clamp(value int) int {
|
||||
return intMax(intMin(value, r.Max), r.Min)
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) InstrumentVoices() *InstrumentVoices { return (*InstrumentVoices)(m) }
|
||||
func (m *Model) TrackVoices() *TrackVoices { return (*TrackVoices)(m) }
|
||||
func (m *Model) SongLength() *SongLength { return (*SongLength)(m) }
|
||||
func (m *Model) BPM() *BPM { return (*BPM)(m) }
|
||||
func (m *Model) RowsPerPattern() *RowsPerPattern { return (*RowsPerPattern)(m) }
|
||||
func (m *Model) RowsPerBeat() *RowsPerBeat { return (*RowsPerBeat)(m) }
|
||||
func (m *Model) Step() *Step { return (*Step)(m) }
|
||||
func (m *Model) Octave() *Octave { return (*Octave)(m) }
|
||||
|
||||
// BeatsPerMinuteInt
|
||||
|
||||
func (v *BPM) Int() Int { return Int{v} }
|
||||
func (v *BPM) Value() int { return v.d.Song.BPM }
|
||||
func (v *BPM) setValue(value int) { v.d.Song.BPM = value }
|
||||
func (v *BPM) Range() intRange { return intRange{1, 999} }
|
||||
func (v *BPM) change(kind string) func() {
|
||||
return (*Model)(v).change("BPMInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// RowsPerPatternInt
|
||||
|
||||
func (v *RowsPerPattern) Int() Int { return Int{v} }
|
||||
func (v *RowsPerPattern) Value() int { return v.d.Song.Score.RowsPerPattern }
|
||||
func (v *RowsPerPattern) setValue(value int) { v.d.Song.Score.RowsPerPattern = value }
|
||||
func (v *RowsPerPattern) Range() intRange { return intRange{1, 256} }
|
||||
func (v *RowsPerPattern) change(kind string) func() {
|
||||
return (*Model)(v).change("RowsPerPatternInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// SongLengthInt
|
||||
|
||||
func (v *SongLength) Int() Int { return Int{v} }
|
||||
func (v *SongLength) Value() int { return v.d.Song.Score.Length }
|
||||
func (v *SongLength) setValue(value int) { v.d.Song.Score.Length = value }
|
||||
func (v *SongLength) Range() intRange { return intRange{1, math.MaxInt32} }
|
||||
func (v *SongLength) change(kind string) func() {
|
||||
return (*Model)(v).change("SongLengthInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// StepInt
|
||||
|
||||
func (v *Step) Int() Int { return Int{v} }
|
||||
func (v *Step) Value() int { return v.d.Step }
|
||||
func (v *Step) setValue(value int) { v.d.Step = value }
|
||||
func (v *Step) Range() intRange { return intRange{0, 8} }
|
||||
func (v *Step) change(kind string) func() {
|
||||
return (*Model)(v).change("StepInt"+kind, NoChange, MinorChange)
|
||||
}
|
||||
|
||||
// OctaveInt
|
||||
|
||||
func (v *Octave) Int() Int { return Int{v} }
|
||||
func (v *Octave) Value() int { return v.d.Octave }
|
||||
func (v *Octave) setValue(value int) { v.d.Octave = value }
|
||||
func (v *Octave) Range() intRange { return intRange{0, 9} }
|
||||
func (v *Octave) change(kind string) func() { return func() {} }
|
||||
|
||||
// RowsPerBeatInt
|
||||
|
||||
func (v *RowsPerBeat) Int() Int { return Int{v} }
|
||||
func (v *RowsPerBeat) Value() int { return v.d.Song.RowsPerBeat }
|
||||
func (v *RowsPerBeat) setValue(value int) { v.d.Song.RowsPerBeat = value }
|
||||
func (v *RowsPerBeat) Range() intRange { return intRange{1, 32} }
|
||||
func (v *RowsPerBeat) change(kind string) func() {
|
||||
return (*Model)(v).change("RowsPerBeatInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// InstrumentVoicesInt
|
||||
|
||||
func (v *InstrumentVoices) Int() Int {
|
||||
return Int{v}
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) Value() int {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return 1
|
||||
}
|
||||
return intMax(v.d.Song.Patch[v.d.InstrIndex].NumVoices, 1)
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) setValue(value int) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].NumVoices = value
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) Range() intRange {
|
||||
return intRange{1, vm.MAX_VOICES - v.d.Song.Patch.NumVoices() + v.Value()}
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentVoicesInt."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
|
||||
// TrackVoicesInt
|
||||
|
||||
func (v *TrackVoices) Int() Int {
|
||||
return Int{v}
|
||||
}
|
||||
|
||||
func (v *TrackVoices) Value() int {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return 1
|
||||
}
|
||||
return intMax(v.d.Song.Score.Tracks[t].NumVoices, 1)
|
||||
}
|
||||
|
||||
func (v *TrackVoices) setValue(value int) {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Score.Tracks[t].NumVoices = value
|
||||
}
|
||||
|
||||
func (v *TrackVoices) Range() intRange {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return intRange{1, 1}
|
||||
}
|
||||
return intRange{1, vm.MAX_VOICES - v.d.Song.Score.NumVoices() + v.d.Song.Score.Tracks[t].NumVoices}
|
||||
}
|
||||
|
||||
func (v *TrackVoices) change(kind string) func() {
|
||||
return (*Model)(v).change("TrackVoicesInt."+kind, ScoreChange, MinorChange)
|
||||
}
|
||||
772
tracker/list.go
Normal file
772
tracker/list.go
Normal file
@ -0,0 +1,772 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type (
|
||||
List struct {
|
||||
ListData
|
||||
}
|
||||
|
||||
ListData interface {
|
||||
Selected() int
|
||||
Selected2() int
|
||||
SetSelected(int)
|
||||
SetSelected2(int)
|
||||
Count() int
|
||||
}
|
||||
|
||||
MutableListData interface {
|
||||
change(kind string, severity ChangeSeverity) func()
|
||||
cancel()
|
||||
swap(i, j int) (ok bool)
|
||||
delete(i int) (ok bool)
|
||||
marshal(from, to int) ([]byte, error)
|
||||
unmarshal([]byte) (from, to int, err error)
|
||||
}
|
||||
|
||||
UnitListItem struct {
|
||||
Type string
|
||||
Disabled bool
|
||||
StackNeed, StackBefore, StackAfter int
|
||||
}
|
||||
|
||||
UnitYieldFunc func(item UnitListItem) (ok bool)
|
||||
UnitSearchYieldFunc func(item string) (ok bool)
|
||||
|
||||
Instruments Model // Instruments is a list of instruments, implementing ListData & MutableListData interfaces
|
||||
Units Model // Units is a list of all the units in the selected instrument, implementing ListData & MutableListData interfaces
|
||||
Tracks Model // Tracks is a list of all the tracks, implementing ListData & MutableListData interfaces
|
||||
OrderRows Model // OrderRows is a list of all the order rows, implementing ListData & MutableListData interfaces
|
||||
NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
|
||||
SearchResults Model // SearchResults is a unmutable list of all the search results, implementing ListData interface
|
||||
Presets Model // Presets is a unmutable list of all the presets, implementing ListData interface
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Instruments() *Instruments { return (*Instruments)(m) }
|
||||
func (m *Model) Units() *Units { return (*Units)(m) }
|
||||
func (m *Model) Tracks() *Tracks { return (*Tracks)(m) }
|
||||
func (m *Model) OrderRows() *OrderRows { return (*OrderRows)(m) }
|
||||
func (m *Model) NoteRows() *NoteRows { return (*NoteRows)(m) }
|
||||
func (m *Model) SearchResults() *SearchResults { return (*SearchResults)(m) }
|
||||
|
||||
// MoveElements moves the selected elements in a list by delta. If delta is
|
||||
// negative, the elements move up, otherwise down. The list must implement the
|
||||
// MutableListData interface.
|
||||
func (v List) MoveElements(delta int) (ok bool) {
|
||||
if delta == 0 {
|
||||
return false
|
||||
}
|
||||
s, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer s.change("MoveElements", MajorChange)()
|
||||
a, b := v.listRange()
|
||||
if a+delta < 0 {
|
||||
delta = -a
|
||||
}
|
||||
if b+delta >= v.Count() {
|
||||
delta = v.Count() - 1 - b
|
||||
}
|
||||
if delta < 0 {
|
||||
for i := a; i <= b; i++ {
|
||||
if !s.swap(i, i+delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := b; i >= a; i-- {
|
||||
if !s.swap(i, i+delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
v.SetSelected(v.Selected() + delta)
|
||||
v.SetSelected2(v.Selected2() + delta)
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteElements deletes the selected elements in a list. The list must
|
||||
// implement the MutableListData interface.
|
||||
func (v List) DeleteElements(backwards bool) (ok bool) {
|
||||
d, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer d.change("DeleteElements", MajorChange)()
|
||||
a, b := v.listRange()
|
||||
for i := b; i >= a; i-- {
|
||||
if !d.delete(i) {
|
||||
d.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
if backwards && a > 0 {
|
||||
a--
|
||||
}
|
||||
v.SetSelected(a)
|
||||
v.SetSelected2(a)
|
||||
return true
|
||||
}
|
||||
|
||||
// CopyElements copies the selected elements in a list. The list must implement
|
||||
// the MutableListData interface. Returns the copied data, marshaled into byte
|
||||
// slice, and true if successful.
|
||||
func (v List) CopyElements() ([]byte, bool) {
|
||||
a, b := v.listRange()
|
||||
m, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
ret, err := m.marshal(a, b)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// PasteElements pastes the data into the list. The data is unmarshaled from the
|
||||
// byte slice. The list must implement the MutableListData interface. Returns
|
||||
// true if successful.
|
||||
func (v List) PasteElements(data []byte) (ok bool) {
|
||||
m, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
defer m.change("PasteElements", MajorChange)()
|
||||
from, to, err := m.unmarshal(data)
|
||||
if err != nil {
|
||||
m.cancel()
|
||||
return false
|
||||
}
|
||||
v.SetSelected(from)
|
||||
v.SetSelected2(to)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *List) listRange() (lower, higher int) {
|
||||
lower = intMin(v.Selected(), v.Selected2())
|
||||
higher = intMax(v.Selected(), v.Selected2())
|
||||
return
|
||||
}
|
||||
|
||||
// Instruments methods
|
||||
|
||||
func (v *Instruments) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *Instruments) Item(i int) (name string, maxLevel float32, ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return "", 0, false
|
||||
}
|
||||
name = v.d.Song.Patch[i].Name
|
||||
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
|
||||
end := start + v.d.Song.Patch[i].NumVoices
|
||||
if end >= vm.MAX_VOICES {
|
||||
end = vm.MAX_VOICES
|
||||
}
|
||||
if start < end {
|
||||
for _, level := range v.voiceLevels[start:end] {
|
||||
if maxLevel < level {
|
||||
maxLevel = level
|
||||
}
|
||||
}
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
func (v *Instruments) FirstID(i int) (id int, ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return 0, false
|
||||
}
|
||||
if len(v.d.Song.Patch[i].Units) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return v.d.Song.Patch[i].Units[0].ID, true
|
||||
}
|
||||
|
||||
func (v *Instruments) Selected() int {
|
||||
return intMax(intMin(v.d.InstrIndex, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) Selected2() int {
|
||||
return intMax(intMin(v.d.InstrIndex2, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) SetSelected(value int) {
|
||||
v.d.InstrIndex = intMax(intMin(value, v.Count()-1), 0)
|
||||
v.d.UnitIndex = 0
|
||||
v.d.UnitIndex2 = 0
|
||||
v.d.UnitSearching = false
|
||||
v.d.UnitSearchString = ""
|
||||
}
|
||||
|
||||
func (v *Instruments) SetSelected2(value int) {
|
||||
v.d.InstrIndex2 = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) swap(i, j int) (ok bool) {
|
||||
if i < 0 || j < 0 || i >= len(v.d.Song.Patch) || j >= len(v.d.Song.Patch) || i == j {
|
||||
return false
|
||||
}
|
||||
instr := v.d.Song.Patch
|
||||
instr[i], instr[j] = instr[j], instr[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Instruments) delete(i int) (ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
v.d.Song.Patch = append(v.d.Song.Patch[:i], v.d.Song.Patch[i+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Instruments) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("InstrumentListView."+n, PatchChange, severity)
|
||||
}
|
||||
|
||||
func (v *Instruments) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Instruments) Count() int {
|
||||
return len(v.d.Song.Patch)
|
||||
}
|
||||
|
||||
func (v *Instruments) marshal(from, to int) ([]byte, error) {
|
||||
if from < 0 || to >= len(v.d.Song.Patch) || from > to {
|
||||
return nil, fmt.Errorf("InstrumentListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Patch sointu.Patch }{v.d.Song.Patch[from : to+1]})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("InstrumentListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Instruments) unmarshal(data []byte) (from, to int, err error) {
|
||||
var newInstr struct{ Patch sointu.Patch }
|
||||
if err := yaml.Unmarshal(data, &newInstr); err != nil {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(newInstr.Patch) == 0 {
|
||||
return 0, 0, errors.New("InstrumentListView.unmarshal: no instruments")
|
||||
}
|
||||
if v.d.Song.Patch.NumVoices()+newInstr.Patch.NumVoices() > vm.MAX_VOICES {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: too many voices: %d", v.d.Song.Patch.NumVoices()+newInstr.Patch.NumVoices())
|
||||
}
|
||||
patch := append(v.d.Song.Patch, make([]sointu.Instrument, len(newInstr.Patch))...)
|
||||
sel := v.Selected()
|
||||
copy(patch[sel+len(newInstr.Patch):], patch[sel:])
|
||||
copy(patch[sel:sel+len(newInstr.Patch)], newInstr.Patch)
|
||||
v.d.Song.Patch = patch
|
||||
from = sel
|
||||
to = sel + len(newInstr.Patch) - 1
|
||||
return
|
||||
}
|
||||
|
||||
// Units methods
|
||||
|
||||
func (v *Units) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (m *Units) SelectedType() string {
|
||||
if m.d.InstrIndex < 0 ||
|
||||
m.d.InstrIndex >= len(m.d.Song.Patch) ||
|
||||
m.d.UnitIndex < 0 ||
|
||||
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||
return ""
|
||||
}
|
||||
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
||||
}
|
||||
|
||||
func (m *Units) SetSelectedType(t string) {
|
||||
if m.d.InstrIndex < 0 ||
|
||||
m.d.InstrIndex >= len(m.d.Song.Patch) ||
|
||||
m.d.UnitIndex < 0 ||
|
||||
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||
return
|
||||
}
|
||||
unit, ok := defaultUnits[t]
|
||||
if !ok { // if the type is invalid, we just set it to empty unit
|
||||
unit = sointu.Unit{Parameters: make(map[string]int)}
|
||||
} else {
|
||||
unit = unit.Copy()
|
||||
}
|
||||
oldUnit := m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex]
|
||||
if oldUnit.Type == unit.Type {
|
||||
return
|
||||
}
|
||||
defer m.change("SetSelectedType", MajorChange)()
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = unit
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit
|
||||
}
|
||||
|
||||
func (v *Units) Iterate(yield UnitYieldFunc) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
stackBefore := 0
|
||||
for _, unit := range v.d.Song.Patch[v.d.InstrIndex].Units {
|
||||
stackAfter := stackBefore + unit.StackChange()
|
||||
if !yield(UnitListItem{
|
||||
Type: unit.Type,
|
||||
Disabled: unit.Disabled,
|
||||
StackNeed: unit.StackNeed(),
|
||||
StackBefore: stackBefore,
|
||||
StackAfter: stackAfter,
|
||||
}) {
|
||||
break
|
||||
}
|
||||
stackBefore = stackAfter
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Units) Selected() int {
|
||||
return intMax(intMin(v.d.UnitIndex, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Units) Selected2() int {
|
||||
return intMax(intMin(v.d.UnitIndex2, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Units) SetSelected(value int) {
|
||||
m := (*Model)(v)
|
||||
m.d.UnitIndex = intMax(intMin(value, v.Count()-1), 0)
|
||||
m.d.ParamIndex = 0
|
||||
m.d.UnitSearching = false
|
||||
m.d.UnitSearchString = ""
|
||||
}
|
||||
|
||||
func (v *Units) SetSelected2(value int) {
|
||||
(*Model)(v).d.UnitIndex2 = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Units) Count() int {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return 0
|
||||
}
|
||||
return len(m.d.Song.Patch[(*Model)(v).d.InstrIndex].Units)
|
||||
}
|
||||
|
||||
func (v *Units) swap(i, j int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
if i < 0 || j < 0 || i >= len(units) || j >= len(units) || i == j {
|
||||
return false
|
||||
}
|
||||
units[i], units[j] = units[j], units[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Units) delete(i int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
if i < 0 || i >= len(units) {
|
||||
return false
|
||||
}
|
||||
units = append(units[:i], units[i+1:]...)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = units
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Units) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("UnitListView."+n, PatchChange, severity)
|
||||
}
|
||||
|
||||
func (v *Units) cancel() {
|
||||
(*Model)(v).changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Units) marshal(from, to int) ([]byte, error) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return nil, errors.New("UnitListView.marshal: no instruments")
|
||||
}
|
||||
if from < 0 || to >= len(m.d.Song.Patch[m.d.InstrIndex].Units) || from > to {
|
||||
return nil, fmt.Errorf("UnitListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{m.d.Song.Patch[m.d.InstrIndex].Units[from : to+1]})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Units) unmarshal(data []byte) (from, to int, err error) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return 0, 0, errors.New("UnitListView.unmarshal: no instruments")
|
||||
}
|
||||
var pastedUnits struct{ Units []sointu.Unit }
|
||||
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
|
||||
return 0, 0, fmt.Errorf("UnitListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(pastedUnits.Units) == 0 {
|
||||
return 0, 0, errors.New("UnitListView.unmarshal: no units")
|
||||
}
|
||||
m.assignUnitIDs(pastedUnits.Units)
|
||||
sel := v.Selected()
|
||||
units := append(m.d.Song.Patch[m.d.InstrIndex].Units, make([]sointu.Unit, len(pastedUnits.Units))...)
|
||||
copy(units[sel+len(pastedUnits.Units):], units[sel:])
|
||||
copy(units[sel:], pastedUnits.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = units
|
||||
from = sel
|
||||
to = sel + len(pastedUnits.Units) - 1
|
||||
return
|
||||
}
|
||||
|
||||
// Tracks methods
|
||||
|
||||
func (v *Tracks) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *Tracks) Selected() int {
|
||||
return intMax(intMin(v.d.Cursor.Track, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) Selected2() int {
|
||||
return intMax(intMin(v.d.Cursor2.Track, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) SetSelected(value int) {
|
||||
v.d.Cursor.Track = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) SetSelected2(value int) {
|
||||
v.d.Cursor2.Track = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) swap(i, j int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if i < 0 || j < 0 || i >= len(m.d.Song.Score.Tracks) || j >= len(m.d.Song.Score.Tracks) || i == j {
|
||||
return false
|
||||
}
|
||||
tracks := m.d.Song.Score.Tracks
|
||||
tracks[i], tracks[j] = tracks[j], tracks[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Tracks) delete(i int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if i < 0 || i >= len(m.d.Song.Score.Tracks) {
|
||||
return false
|
||||
}
|
||||
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks[:i], m.d.Song.Score.Tracks[i+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Tracks) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("TrackList."+n, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *Tracks) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Tracks) Count() int {
|
||||
return len((*Model)(v).d.Song.Score.Tracks)
|
||||
}
|
||||
|
||||
func (v *Tracks) marshal(from, to int) ([]byte, error) {
|
||||
m := (*Model)(v)
|
||||
if from < 0 || to >= len(m.d.Song.Score.Tracks) || from > to {
|
||||
return nil, fmt.Errorf("TrackListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Score sointu.Score }{sointu.Score{Tracks: m.d.Song.Score.Tracks[from : to+1]}})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TrackListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Tracks) unmarshal(data []byte) (from, to int, err error) {
|
||||
m := (*Model)(v)
|
||||
var newTracks struct{ Score sointu.Score }
|
||||
if err := yaml.Unmarshal(data, &newTracks); err != nil {
|
||||
return 0, 0, fmt.Errorf("TrackListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(newTracks.Score.Tracks) == 0 {
|
||||
return 0, 0, errors.New("TrackListView.unmarshal: no tracks")
|
||||
}
|
||||
if v.d.Song.Score.NumVoices()+newTracks.Score.NumVoices() > vm.MAX_VOICES {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: too many voices: %d", v.d.Song.Patch.NumVoices()+newTracks.Score.NumVoices())
|
||||
}
|
||||
from = m.d.Cursor.Track
|
||||
to = m.d.Cursor.Track + len(newTracks.Score.Tracks) - 1
|
||||
tracks := m.d.Song.Score.Tracks
|
||||
newTracks.Score.Tracks = append(newTracks.Score.Tracks, tracks[m.d.Cursor.Track:]...)
|
||||
tracks = append(tracks[:m.d.Cursor.Track], newTracks.Score.Tracks...)
|
||||
m.d.Song.Score.Tracks = tracks
|
||||
return
|
||||
}
|
||||
|
||||
// OrderRows methods
|
||||
|
||||
func (v *OrderRows) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *OrderRows) Selected() int {
|
||||
p := v.d.Cursor.OrderRow
|
||||
p = intMax(intMin(p, v.Count()-1), 0)
|
||||
return p
|
||||
}
|
||||
|
||||
func (v *OrderRows) Selected2() int {
|
||||
p := v.d.Cursor2.OrderRow
|
||||
p = intMax(intMin(p, v.Count()-1), 0)
|
||||
return p
|
||||
}
|
||||
|
||||
func (v *OrderRows) SetSelected(value int) {
|
||||
y := intMax(intMin(value, v.Count()-1), 0)
|
||||
if y != v.d.Cursor.OrderRow {
|
||||
v.noteTracking = false
|
||||
}
|
||||
v.d.Cursor.OrderRow = y
|
||||
}
|
||||
|
||||
func (v *OrderRows) SetSelected2(value int) {
|
||||
v.d.Cursor2.OrderRow = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *OrderRows) swap(x, y int) (ok bool) {
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
track := &v.d.Song.Score.Tracks[i]
|
||||
a, b := track.Order.Get(x), track.Order.Get(y)
|
||||
track.Order.Set(x, b)
|
||||
track.Order.Set(y, a)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *OrderRows) delete(i int) (ok bool) {
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
if i < len(track.Order) {
|
||||
track.Order = append(track.Order[:i], track.Order[i+1:]...)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *OrderRows) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("OrderRowList."+n, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *OrderRows) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (v *OrderRows) Count() int {
|
||||
return v.d.Song.Score.Length
|
||||
}
|
||||
|
||||
type marshalOrderRows struct {
|
||||
Columns [][]int `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (v *OrderRows) marshal(from, to int) ([]byte, error) {
|
||||
var table marshalOrderRows
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
table.Columns = append(table.Columns, make([]int, to-from+1))
|
||||
for j := 0; j < to-from+1; j++ {
|
||||
table.Columns[i][j] = v.d.Song.Score.Tracks[i].Order.Get(from + j)
|
||||
}
|
||||
}
|
||||
return yaml.Marshal(table)
|
||||
}
|
||||
|
||||
func (v *OrderRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
var table marshalOrderRows
|
||||
err = yaml.Unmarshal(data, &table)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(table.Columns) == 0 {
|
||||
err = errors.New("OrderRowList.unmarshal: no rows")
|
||||
return
|
||||
}
|
||||
from = v.d.Cursor.OrderRow
|
||||
to = v.d.Cursor.OrderRow + len(table.Columns[0]) - 1
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
if i >= len(table.Columns) {
|
||||
break
|
||||
}
|
||||
order := &v.d.Song.Score.Tracks[i].Order
|
||||
for j := 0; j < from-len(*order); j++ {
|
||||
*order = append(*order, -1)
|
||||
}
|
||||
if len(*order) > from {
|
||||
table.Columns[i] = append(table.Columns[i], (*order)[from:]...)
|
||||
*order = (*order)[:from]
|
||||
}
|
||||
*order = append(*order, table.Columns[i]...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NoteRows methods
|
||||
|
||||
func (v *NoteRows) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *NoteRows) Selected() int {
|
||||
return v.d.Song.Score.SongRow(v.d.Song.Score.Clamp(v.d.Cursor.SongPos))
|
||||
}
|
||||
|
||||
func (v *NoteRows) Selected2() int {
|
||||
return v.d.Song.Score.SongRow(v.d.Song.Score.Clamp(v.d.Cursor2.SongPos))
|
||||
}
|
||||
|
||||
func (v *NoteRows) SetSelected(value int) {
|
||||
if value != v.d.Song.Score.SongRow(v.d.Cursor.SongPos) {
|
||||
v.noteTracking = false
|
||||
}
|
||||
v.d.Cursor.SongPos = v.d.Song.Score.Clamp(v.d.Song.Score.SongPos(value))
|
||||
}
|
||||
|
||||
func (v *NoteRows) SetSelected2(value int) {
|
||||
v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(v.d.Song.Score.SongPos(value))
|
||||
|
||||
}
|
||||
|
||||
func (v *NoteRows) swap(i, j int) (ok bool) {
|
||||
ipos := v.d.Song.Score.SongPos(i)
|
||||
jpos := v.d.Song.Score.SongPos(j)
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
n1 := track.Note(ipos)
|
||||
n2 := track.Note(jpos)
|
||||
track.SetNote(ipos, n2)
|
||||
track.SetNote(jpos, n1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *NoteRows) delete(i int) (ok bool) {
|
||||
if i < 0 || i >= v.Count() {
|
||||
return
|
||||
}
|
||||
pos := v.d.Song.Score.SongPos(i)
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
track.SetNote(pos, 1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *NoteRows) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("NoteRowList."+n, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *NoteRows) cancel() {
|
||||
(*Model)(v).changeCancel = true
|
||||
}
|
||||
|
||||
func (v *NoteRows) Count() int {
|
||||
return (*Model)(v).d.Song.Score.Length * v.d.Song.Score.RowsPerPattern
|
||||
}
|
||||
|
||||
type marshalNoteRows struct {
|
||||
NoteRows [][]byte `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (v *NoteRows) marshal(from, to int) ([]byte, error) {
|
||||
var table marshalNoteRows
|
||||
for i, track := range v.d.Song.Score.Tracks {
|
||||
table.NoteRows = append(table.NoteRows, make([]byte, to-from+1))
|
||||
for j := 0; j < to-from+1; j++ {
|
||||
row := from + j
|
||||
pos := v.d.Song.Score.SongPos(row)
|
||||
table.NoteRows[i][j] = track.Note(pos)
|
||||
}
|
||||
}
|
||||
return yaml.Marshal(table)
|
||||
}
|
||||
|
||||
func (v *NoteRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
var table marshalNoteRows
|
||||
if err := yaml.Unmarshal(data, &table); err != nil {
|
||||
return 0, 0, fmt.Errorf("NoteRowList.unmarshal: %v", err)
|
||||
}
|
||||
if len(table.NoteRows) < 1 {
|
||||
return 0, 0, errors.New("NoteRowList.unmarshal: no tracks")
|
||||
}
|
||||
from = v.d.Song.Score.SongRow(v.d.Cursor.SongPos)
|
||||
for i, arr := range table.NoteRows {
|
||||
if i >= len(v.d.Song.Score.Tracks) {
|
||||
continue
|
||||
}
|
||||
to = from + len(arr) - 1
|
||||
for j, note := range arr {
|
||||
y := j + from
|
||||
pos := v.d.Song.Score.SongPos(y)
|
||||
v.d.Song.Score.Tracks[i].SetNote(pos, note)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SearchResults
|
||||
|
||||
func (v *SearchResults) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (l *SearchResults) Iterate(yield UnitSearchYieldFunc) {
|
||||
for _, name := range sointu.UnitNames {
|
||||
if !strings.HasPrefix(name, l.d.UnitSearchString) {
|
||||
continue
|
||||
}
|
||||
if !yield(name) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SearchResults) Selected() int {
|
||||
return intMax(intMin(l.d.UnitSearchIndex, l.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (l *SearchResults) Selected2() int {
|
||||
return intMax(intMin(l.d.UnitSearchIndex, l.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (l *SearchResults) SetSelected(value int) {
|
||||
l.d.UnitSearchIndex = intMax(intMin(value, l.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (l *SearchResults) SetSelected2(value int) {
|
||||
}
|
||||
|
||||
func (l *SearchResults) Count() (count int) {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, l.d.UnitSearchString) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
1619
tracker/model.go
1619
tracker/model.go
File diff suppressed because it is too large
Load Diff
252
tracker/model_test.go
Normal file
252
tracker/model_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
package tracker_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type NullContext struct{}
|
||||
|
||||
func (NullContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
|
||||
return tracker.MIDINoteEvent{}, false
|
||||
}
|
||||
|
||||
func (NullContext) BPM() (bpm float64, ok bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
type modelFuzzState struct {
|
||||
model *tracker.Model
|
||||
clipboard []byte
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
// Ints
|
||||
s.IterateInt("InstrumentVoices", s.model.InstrumentVoices().Int(), yield, seed)
|
||||
s.IterateInt("TrackVoices", s.model.TrackVoices().Int(), yield, seed)
|
||||
s.IterateInt("SongLength", s.model.SongLength().Int(), yield, seed)
|
||||
s.IterateInt("BPM", s.model.BPM().Int(), yield, seed)
|
||||
s.IterateInt("RowsPerPattern", s.model.RowsPerPattern().Int(), yield, seed)
|
||||
s.IterateInt("RowsPerBeat", s.model.RowsPerBeat().Int(), yield, seed)
|
||||
s.IterateInt("Step", s.model.Step().Int(), yield, seed)
|
||||
s.IterateInt("Octave", s.model.Octave().Int(), yield, seed)
|
||||
// Lists
|
||||
s.IterateList("Instruments", s.model.Instruments().List(), yield, seed)
|
||||
s.IterateList("Units", s.model.Units().List(), yield, seed)
|
||||
s.IterateList("Tracks", s.model.Tracks().List(), yield, seed)
|
||||
s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed)
|
||||
s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed)
|
||||
s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed)
|
||||
s.IterateBool("Panic", s.model.Panic().Bool(), yield, seed)
|
||||
s.IterateBool("Recording", s.model.IsRecording().Bool(), yield, seed)
|
||||
s.IterateBool("Playing", s.model.Playing().Bool(), yield, seed)
|
||||
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged().Bool(), yield, seed)
|
||||
s.IterateBool("Effect", s.model.Effect().Bool(), yield, seed)
|
||||
s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed)
|
||||
s.IterateBool("NoteTracking", s.model.NoteTracking().Bool(), yield, seed)
|
||||
// Strings
|
||||
s.IterateString("FilePath", s.model.FilePath().String(), yield, seed)
|
||||
s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed)
|
||||
s.IterateString("InstrumentComment", s.model.InstrumentComment().String(), yield, seed)
|
||||
s.IterateString("UnitSearchText", s.model.UnitSearch().String(), yield, seed)
|
||||
// Actions
|
||||
s.IterateAction("AddTrack", s.model.AddTrack(), yield, seed)
|
||||
s.IterateAction("DeleteTrack", s.model.DeleteTrack(), yield, seed)
|
||||
s.IterateAction("AddInstrument", s.model.AddInstrument(), yield, seed)
|
||||
s.IterateAction("DeleteInstrument", s.model.DeleteInstrument(), yield, seed)
|
||||
s.IterateAction("AddUnitAfter", s.model.AddUnit(false), yield, seed)
|
||||
s.IterateAction("AddUnitBefore", s.model.AddUnit(true), yield, seed)
|
||||
s.IterateAction("DeleteUnit", s.model.DeleteUnit(), yield, seed)
|
||||
s.IterateAction("ClearUnit", s.model.ClearUnit(), yield, seed)
|
||||
s.IterateAction("Undo", s.model.Undo(), yield, seed)
|
||||
s.IterateAction("Redo", s.model.Redo(), yield, seed)
|
||||
s.IterateAction("RemoveUnused", s.model.RemoveUnused(), yield, seed)
|
||||
s.IterateAction("AddSemitone", s.model.AddSemitone(), yield, seed)
|
||||
s.IterateAction("SubtractSemitone", s.model.SubtractSemitone(), yield, seed)
|
||||
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
|
||||
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
|
||||
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
|
||||
s.IterateAction("Rewind", s.model.Rewind(), yield, seed)
|
||||
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
|
||||
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
||||
// Tables
|
||||
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
||||
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateInt(name string, i tracker.Int, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
r := i.Range()
|
||||
yield(name+".Set", func(p string, t *testing.T) {
|
||||
i.Set(seed%(r.Max-r.Min+10) - 5 + r.Min)
|
||||
})
|
||||
yield(name+".Value", func(p string, t *testing.T) {
|
||||
if v := i.Value(); v < r.Min || v > r.Max {
|
||||
r := i.Range()
|
||||
t.Errorf("Path: %s %s value out of range [%d,%d]: %d", p, name, r.Min, r.Max, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateAction(name string, a tracker.Action, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".Do", func(p string, t *testing.T) {
|
||||
a.Do()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateBool(name string, b tracker.Bool, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".Set", func(p string, t *testing.T) {
|
||||
b.Set(seed%2 == 0)
|
||||
})
|
||||
yield(name+".Toggle", func(p string, t *testing.T) {
|
||||
b.Toggle()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateString(name string, str tracker.String, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".Set", func(p string, t *testing.T) {
|
||||
str.Set(fmt.Sprintf("%d", seed))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateList(name string, l tracker.List, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".SetSelected", func(p string, t *testing.T) {
|
||||
l.SetSelected(seed%50 - 16)
|
||||
})
|
||||
yield(name+".Count", func(p string, t *testing.T) {
|
||||
if c := l.Count(); c > 0 {
|
||||
if l.Selected() < 0 || l.Selected() >= c {
|
||||
t.Errorf("Path: %s %s selected out of range: %d", p, name, l.Selected())
|
||||
}
|
||||
} else {
|
||||
if l.Selected() != 0 {
|
||||
t.Errorf("Path: %s %s selected out of range: %d", p, name, l.Selected())
|
||||
}
|
||||
}
|
||||
})
|
||||
yield(name+".SetSelected2", func(p string, t *testing.T) {
|
||||
l.SetSelected2(seed%50 - 16)
|
||||
})
|
||||
yield(name+".Count2", func(p string, t *testing.T) {
|
||||
if c := l.Count(); c > 0 {
|
||||
if l.Selected2() < 0 || l.Selected2() >= c {
|
||||
t.Errorf("Path: %s List selected2 out of range: %d", p, l.Selected2())
|
||||
}
|
||||
} else {
|
||||
if l.Selected2() != 0 {
|
||||
t.Errorf("Path: %s List selected2 out of range: %d", p, l.Selected2())
|
||||
}
|
||||
}
|
||||
})
|
||||
yield(name+".MoveElements", func(p string, t *testing.T) {
|
||||
l.MoveElements(seed%2*2 - 1)
|
||||
})
|
||||
yield(name+".DeleteElementsForward", func(p string, t *testing.T) {
|
||||
l.DeleteElements(false)
|
||||
})
|
||||
yield(name+".DeleteElementsBackward", func(p string, t *testing.T) {
|
||||
l.DeleteElements(true)
|
||||
})
|
||||
yield(name+".CopyElements", func(p string, t *testing.T) {
|
||||
s.clipboard, _ = l.CopyElements()
|
||||
})
|
||||
yield(name+".PasteElements", func(p string, t *testing.T) {
|
||||
l.PasteElements(s.clipboard)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateTable(name string, table tracker.Table, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".SetCursor", func(p string, t *testing.T) {
|
||||
table.SetCursor(tracker.Point{seed % 16, seed * 1337 % 16})
|
||||
})
|
||||
yield(name+".SetCursor2", func(p string, t *testing.T) {
|
||||
table.SetCursor2(tracker.Point{seed % 16, seed * 1337 % 16})
|
||||
})
|
||||
yield(name+".Cursor", func(p string, t *testing.T) {
|
||||
if c := table.Cursor(); c.X < 0 || (c.X >= table.Width() && table.Width() > 0) || c.Y < 0 || (c.Y >= table.Height() && table.Height() > 0) {
|
||||
t.Errorf("Path: %s Table cursor out of range: %v", p, c)
|
||||
}
|
||||
})
|
||||
yield(name+".Cursor2", func(p string, t *testing.T) {
|
||||
if c := table.Cursor2(); c.X < 0 || (c.X >= table.Width() && table.Width() > 0) || c.Y < 0 || (c.Y >= table.Height() && table.Height() > 0) {
|
||||
t.Errorf("Path: %s Table cursor2 out of range: %v", p, c)
|
||||
}
|
||||
})
|
||||
yield(name+".SetCursorX", func(p string, t *testing.T) {
|
||||
table.SetCursorX(seed % 16)
|
||||
})
|
||||
yield(name+".SetCursorY", func(p string, t *testing.T) {
|
||||
table.SetCursorY(seed % 16)
|
||||
})
|
||||
yield(name+".MoveCursor", func(p string, t *testing.T) {
|
||||
table.MoveCursor(seed%2*2-1, seed%2*2-1)
|
||||
})
|
||||
yield(name+".Copy", func(p string, t *testing.T) {
|
||||
s.clipboard, _ = table.Copy()
|
||||
})
|
||||
yield(name+".Paste", func(p string, t *testing.T) {
|
||||
table.Paste(s.clipboard)
|
||||
})
|
||||
yield(name+".Clear", func(p string, t *testing.T) {
|
||||
table.Clear()
|
||||
})
|
||||
yield(name+".Fill", func(p string, t *testing.T) {
|
||||
table.Fill(seed % 16)
|
||||
})
|
||||
yield(name+".Add", func(p string, t *testing.T) {
|
||||
table.Add(seed % 16)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzModel(f *testing.F) {
|
||||
seed := make([]byte, 1)
|
||||
for i := range seed {
|
||||
seed[i] = byte(i)
|
||||
}
|
||||
f.Add(seed)
|
||||
f.Fuzz(func(t *testing.T, slice []byte) {
|
||||
reader := bytes.NewReader(slice)
|
||||
synther := vm.GoSynther{}
|
||||
model, player := tracker.NewModelPlayer(synther, "")
|
||||
buf := make([][2]float32, 2048)
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-closeChan:
|
||||
break loop
|
||||
default:
|
||||
ctx := NullContext{}
|
||||
player.Process(buf, ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
state := modelFuzzState{model: model}
|
||||
count := 0
|
||||
state.Iterate(func(n string, f func(p string, t *testing.T)) bool {
|
||||
count++
|
||||
return true
|
||||
}, 0)
|
||||
totalPath := ""
|
||||
for m, err := binary.ReadVarint(reader); err == nil; m, err = binary.ReadVarint(reader) {
|
||||
seed := int(m)
|
||||
index := seed % count
|
||||
state.Iterate(func(n string, f func(p string, t *testing.T)) bool {
|
||||
if index == 0 {
|
||||
totalPath += n + ". "
|
||||
f(totalPath, t)
|
||||
}
|
||||
index--
|
||||
return index > 0
|
||||
}, seed)
|
||||
}
|
||||
closeChan <- struct{}{}
|
||||
})
|
||||
}
|
||||
345
tracker/params.go
Normal file
345
tracker/params.go
Normal file
@ -0,0 +1,345 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
Parameter interface {
|
||||
IntData
|
||||
Type() ParameterType
|
||||
Name() string
|
||||
Hint() string
|
||||
LargeStep() int
|
||||
Reset()
|
||||
}
|
||||
|
||||
parameter struct {
|
||||
m *Model
|
||||
unit *sointu.Unit
|
||||
}
|
||||
|
||||
NamedParameter struct {
|
||||
parameter
|
||||
up *sointu.UnitParameter
|
||||
}
|
||||
|
||||
DelayTimeParameter struct {
|
||||
parameter
|
||||
index int
|
||||
}
|
||||
|
||||
DelayLinesParameter struct{ parameter }
|
||||
GmDlsEntryParameter struct{ parameter }
|
||||
ReverbParameter struct{ parameter }
|
||||
|
||||
Params Model
|
||||
|
||||
ParamYieldFunc func(Parameter)
|
||||
|
||||
ParameterType int
|
||||
)
|
||||
|
||||
const (
|
||||
IntegerParameter ParameterType = iota
|
||||
BoolParameter
|
||||
IDParameter
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Params() *Params { return (*Params)(m) }
|
||||
|
||||
// parameter methods
|
||||
|
||||
func (p parameter) change(kind string) func() {
|
||||
return p.m.change("Parameter."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
|
||||
// ParamList
|
||||
|
||||
func (pl *Params) List() List { return List{pl} }
|
||||
func (pl *Params) Selected() int { return pl.d.ParamIndex }
|
||||
func (pl *Params) Selected2() int { return pl.Selected() }
|
||||
func (pl *Params) SetSelected(value int) { pl.d.ParamIndex = intMax(intMin(value, pl.Count()-1), 0) }
|
||||
func (pl *Params) SetSelected2(value int) {}
|
||||
func (pl *Params) cancel() { (*Model)(pl).changeCancel = true }
|
||||
|
||||
func (pl *Params) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(pl).change("ParamList."+n, PatchChange, severity)
|
||||
}
|
||||
|
||||
func (pl *Params) Count() int {
|
||||
count := 0
|
||||
pl.Iterate(func(p Parameter) {
|
||||
count++
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
func (pl *Params) SelectedItem() (ret Parameter) {
|
||||
index := pl.Selected()
|
||||
pl.Iterate(func(param Parameter) {
|
||||
if index == 0 {
|
||||
ret = param
|
||||
}
|
||||
index--
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (pl *Params) Iterate(yield ParamYieldFunc) {
|
||||
if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) {
|
||||
return
|
||||
}
|
||||
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex]
|
||||
unitType, ok := sointu.UnitTypes[unit.Type]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i := range unitType {
|
||||
if !unitType[i].CanSet {
|
||||
continue
|
||||
}
|
||||
if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 {
|
||||
break // don't show the sample related params unless necessary
|
||||
}
|
||||
yield(NamedParameter{
|
||||
parameter: parameter{m: (*Model)(pl), unit: unit},
|
||||
up: &unitType[i],
|
||||
})
|
||||
}
|
||||
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
|
||||
yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
|
||||
}
|
||||
switch {
|
||||
case unit.Type == "delay":
|
||||
if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 {
|
||||
unit.VarArgs = append(unit.VarArgs, 1)
|
||||
}
|
||||
yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
|
||||
yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
|
||||
for i := range unit.VarArgs {
|
||||
yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NamedParameter
|
||||
|
||||
func (p NamedParameter) Name() string { return p.up.Name }
|
||||
func (p NamedParameter) Range() intRange { return intRange{Min: p.up.MinValue, Max: p.up.MaxValue} }
|
||||
func (p NamedParameter) Value() int { return p.unit.Parameters[p.up.Name] }
|
||||
func (p NamedParameter) setValue(value int) { p.unit.Parameters[p.up.Name] = value }
|
||||
|
||||
func (p NamedParameter) Reset() {
|
||||
v, ok := defaultUnits[p.unit.Type].Parameters[p.up.Name]
|
||||
if !ok || p.unit.Parameters[p.up.Name] == v {
|
||||
return
|
||||
}
|
||||
defer p.parameter.change("Reset")()
|
||||
p.unit.Parameters[p.up.Name] = v
|
||||
}
|
||||
|
||||
func (p NamedParameter) Type() ParameterType {
|
||||
if p.unit.Type == "send" && p.up.Name == "target" {
|
||||
return IDParameter
|
||||
}
|
||||
if p.up.MinValue == 0 && p.up.MaxValue == 1 {
|
||||
return BoolParameter
|
||||
}
|
||||
return IntegerParameter
|
||||
}
|
||||
|
||||
func (p NamedParameter) Hint() string {
|
||||
val := p.Value()
|
||||
text := p.m.d.Song.Patch.ParamHintString(p.m.d.InstrIndex, p.m.d.UnitIndex, p.up.Name)
|
||||
if text != "" {
|
||||
text = fmt.Sprintf("%v / %v", val, text)
|
||||
} else {
|
||||
text = strconv.Itoa(val)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (p NamedParameter) LargeStep() int {
|
||||
if p.up.Name == "transpose" {
|
||||
return 12
|
||||
}
|
||||
return 16
|
||||
}
|
||||
|
||||
// GmDlsEntryParameter
|
||||
|
||||
func (p GmDlsEntryParameter) Name() string { return "sample" }
|
||||
func (p GmDlsEntryParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p GmDlsEntryParameter) Range() intRange { return intRange{Min: 0, Max: len(GmDlsEntries)} }
|
||||
func (p GmDlsEntryParameter) LargeStep() int { return 16 }
|
||||
func (p GmDlsEntryParameter) Reset() { return }
|
||||
|
||||
func (p GmDlsEntryParameter) Value() int {
|
||||
key := vm.SampleOffset{Start: uint32(p.unit.Parameters["samplestart"]), LoopStart: uint16(p.unit.Parameters["loopstart"]), LoopLength: uint16(p.unit.Parameters["looplength"])}
|
||||
if v, ok := gmDlsEntryMap[key]; ok {
|
||||
return v + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p GmDlsEntryParameter) setValue(v int) {
|
||||
if v < 1 || v > len(GmDlsEntries) {
|
||||
return
|
||||
}
|
||||
e := GmDlsEntries[v-1]
|
||||
p.unit.Parameters["samplestart"] = e.Start
|
||||
p.unit.Parameters["loopstart"] = e.LoopStart
|
||||
p.unit.Parameters["looplength"] = e.LoopLength
|
||||
p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose
|
||||
}
|
||||
|
||||
func (p GmDlsEntryParameter) Hint() string {
|
||||
if v := p.Value(); v > 0 {
|
||||
return fmt.Sprintf("%v / %v", v, GmDlsEntries[v-1].Name)
|
||||
}
|
||||
return "0 / custom"
|
||||
}
|
||||
|
||||
// DelayTimeParameter
|
||||
|
||||
func (p DelayTimeParameter) Name() string { return "delaytime" }
|
||||
func (p DelayTimeParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p DelayTimeParameter) LargeStep() int { return 16 }
|
||||
func (p DelayTimeParameter) Reset() { return }
|
||||
|
||||
func (p DelayTimeParameter) Value() int {
|
||||
if p.index < 0 || p.index >= len(p.unit.VarArgs) {
|
||||
return 1
|
||||
}
|
||||
return p.unit.VarArgs[p.index]
|
||||
}
|
||||
|
||||
func (p DelayTimeParameter) setValue(v int) {
|
||||
p.unit.VarArgs[p.index] = v
|
||||
}
|
||||
|
||||
func (p DelayTimeParameter) Range() intRange {
|
||||
if p.unit.Parameters["notetracking"] == 2 {
|
||||
return intRange{Min: 1, Max: 576}
|
||||
}
|
||||
return intRange{Min: 1, Max: 65535}
|
||||
}
|
||||
|
||||
func (p DelayTimeParameter) Hint() string {
|
||||
val := p.Value()
|
||||
var text string
|
||||
switch p.unit.Parameters["notetracking"] {
|
||||
default:
|
||||
case 0:
|
||||
text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(p.m.d.Song.SamplesPerRow()))
|
||||
case 1:
|
||||
relPitch := float64(val) / 10787
|
||||
semitones := -math.Log2(relPitch) * 12
|
||||
text = fmt.Sprintf("%v / %.3f st", val, semitones)
|
||||
case 2:
|
||||
k := 0
|
||||
v := val
|
||||
for v&1 == 0 { // divide val by 2 until it is odd
|
||||
v >>= 1
|
||||
k++
|
||||
}
|
||||
switch v {
|
||||
case 1:
|
||||
if k <= 7 {
|
||||
text = fmt.Sprintf(" (1/%d triplet)", 1<<(7-k))
|
||||
}
|
||||
case 3:
|
||||
if k <= 6 {
|
||||
text = fmt.Sprintf(" (1/%d)", 1<<(6-k))
|
||||
}
|
||||
break
|
||||
case 9:
|
||||
if k <= 5 {
|
||||
text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k))
|
||||
}
|
||||
}
|
||||
text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text)
|
||||
}
|
||||
if p.unit.Parameters["stereo"] == 1 {
|
||||
if p.index < len(p.unit.VarArgs)/2 {
|
||||
text += " R"
|
||||
} else {
|
||||
text += " L"
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// DelayLinesParameter
|
||||
|
||||
func (p DelayLinesParameter) Name() string { return "delaylines" }
|
||||
func (p DelayLinesParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p DelayLinesParameter) Range() intRange { return intRange{Min: 1, Max: 32} }
|
||||
func (p DelayLinesParameter) LargeStep() int { return 4 }
|
||||
func (p DelayLinesParameter) Reset() { return }
|
||||
func (p DelayLinesParameter) Hint() string { return strconv.Itoa(p.Value()) }
|
||||
|
||||
func (p DelayLinesParameter) Value() int {
|
||||
val := len(p.unit.VarArgs)
|
||||
if p.unit.Parameters["stereo"] == 1 {
|
||||
val /= 2
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p DelayLinesParameter) setValue(v int) {
|
||||
targetLines := v
|
||||
if p.unit.Parameters["stereo"] == 1 {
|
||||
targetLines *= 2
|
||||
}
|
||||
for len(p.unit.VarArgs) < targetLines {
|
||||
p.unit.VarArgs = append(p.unit.VarArgs, 1)
|
||||
}
|
||||
p.unit.VarArgs = p.unit.VarArgs[:targetLines]
|
||||
}
|
||||
|
||||
// ReverbParameter
|
||||
|
||||
func (p ReverbParameter) Name() string { return "reverb" }
|
||||
func (p ReverbParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p ReverbParameter) Range() intRange { return intRange{Min: 0, Max: len(reverbs)} }
|
||||
func (p ReverbParameter) LargeStep() int { return 1 }
|
||||
func (p ReverbParameter) Reset() { return }
|
||||
|
||||
func (p ReverbParameter) Value() int {
|
||||
i := slices.IndexFunc(reverbs, func(d delayPreset) bool {
|
||||
return d.stereo == p.unit.Parameters["stereo"] && p.unit.Parameters["notetracking"] == 0 && slices.Equal(d.varArgs, p.unit.VarArgs)
|
||||
})
|
||||
return i + 1
|
||||
}
|
||||
|
||||
func (p ReverbParameter) setValue(v int) {
|
||||
if v < 1 || v > len(reverbs) {
|
||||
return
|
||||
}
|
||||
entry := reverbs[v-1]
|
||||
p.unit.Parameters["stereo"] = entry.stereo
|
||||
p.unit.Parameters["notetracking"] = 0
|
||||
p.unit.VarArgs = make([]int, len(entry.varArgs))
|
||||
copy(p.unit.VarArgs, entry.varArgs)
|
||||
}
|
||||
|
||||
func (p ReverbParameter) Hint() string {
|
||||
i := p.Value()
|
||||
if i > 0 {
|
||||
return fmt.Sprintf("%v / %v", i, reverbs[i-1].name)
|
||||
}
|
||||
return "0 / custom"
|
||||
}
|
||||
@ -19,18 +19,19 @@ type (
|
||||
song sointu.Song // the song being played
|
||||
playing bool // is the player playing the score or not
|
||||
rowtime int // how many samples have been played in the current row
|
||||
position ScoreRow // the current position in the score
|
||||
songPos sointu.SongPos // the current position in the score
|
||||
avgVolumeMeter VolumeAnalyzer // the volume analyzer used to calculate the average volume
|
||||
peakVolumeMeter VolumeAnalyzer // the volume analyzer used to calculate the peak volume
|
||||
voiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
|
||||
voices [vm.MAX_VOICES]voice
|
||||
loop Loop
|
||||
|
||||
recState recState // is the recording off; are we waiting for a note; or are we recording
|
||||
recording Recording // the recorded MIDI events and BPM
|
||||
|
||||
synther sointu.Synther // the synther used to create new synths
|
||||
playerMessages chan<- PlayerMessage
|
||||
modelMessages <-chan interface{}
|
||||
synther sointu.Synther // the synther used to create new synths
|
||||
playerMsgs chan<- PlayerMsg
|
||||
modelMsgs <-chan interface{}
|
||||
}
|
||||
|
||||
// PlayerProcessContext is the context given to the player when processing
|
||||
@ -50,29 +51,19 @@ type (
|
||||
Note byte
|
||||
}
|
||||
|
||||
// PlayerMessage is a message sent from the player to the model. The Inner
|
||||
// PlayerMsg is a message sent from the player to the model. The Inner
|
||||
// field can contain any message. Panic, AverageVolume, PeakVolume, SongRow
|
||||
// and VoiceStates transmitted frequently, with every message, so they are
|
||||
// treated specially, to avoid boxing. All the rest messages can be boxed to
|
||||
// Inner interface{}
|
||||
PlayerMessage struct {
|
||||
PlayerMsg struct {
|
||||
Panic bool
|
||||
AverageVolume Volume
|
||||
PeakVolume Volume
|
||||
SongRow ScoreRow
|
||||
SongPosition sointu.SongPos
|
||||
VoiceLevels [vm.MAX_VOICES]float32
|
||||
Inner interface{}
|
||||
}
|
||||
|
||||
// PlayerCrashMessage is sent to the model when the player crashes.
|
||||
PlayerCrashMessage struct {
|
||||
error
|
||||
}
|
||||
|
||||
// PlayerVolumeErrorMessage is sent to the model there is an error in the volume analyzer. The error is not fatal.
|
||||
PlayerVolumeErrorMessage struct {
|
||||
error
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
@ -93,20 +84,6 @@ const (
|
||||
|
||||
const numRenderTries = 10000
|
||||
|
||||
// NewPlayer creates a new player. The playerMessages channel is used to send
|
||||
// messages to the model. The modelMessages channel is used to receive messages
|
||||
// from the model. The synther is used to create new synths.
|
||||
func NewPlayer(synther sointu.Synther, playerMessages chan<- PlayerMessage, modelMessages <-chan interface{}) *Player {
|
||||
p := &Player{
|
||||
playerMessages: playerMessages,
|
||||
modelMessages: modelMessages,
|
||||
synther: synther,
|
||||
avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, Min: -100, Max: 20},
|
||||
peakVolumeMeter: VolumeAnalyzer{Attack: 1e-4, Release: 1, Min: -100, Max: 20},
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Process renders audio to the given buffer, trying to fill it completely. If
|
||||
// the buffer is not filled, the synth is destroyed and an error is sent to the
|
||||
// model. context tells the player which MIDI events happen during the current
|
||||
@ -152,6 +129,9 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
if p.playing {
|
||||
timeUntilRowAdvance = p.song.SamplesPerRow() - p.rowtime
|
||||
}
|
||||
if timeUntilRowAdvance < 0 {
|
||||
timeUntilRowAdvance = 0
|
||||
}
|
||||
var rendered, timeAdvanced int
|
||||
var err error
|
||||
if p.synth != nil {
|
||||
@ -169,7 +149,7 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
}
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synth.Render: %w", err)})
|
||||
p.send(Alert{Message: fmt.Sprintf("synth.Render: %s", err.Error()), Priority: Error, Name: "PlayerCrash"})
|
||||
}
|
||||
buffer = buffer[rendered:]
|
||||
frame += rendered
|
||||
@ -189,47 +169,41 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
if len(buffer) == 0 {
|
||||
err := p.avgVolumeMeter.Update(oldBuffer)
|
||||
err2 := p.peakVolumeMeter.Update(oldBuffer)
|
||||
var msg interface{}
|
||||
if err != nil {
|
||||
msg = PlayerCrashMessage{err}
|
||||
p.synth = nil
|
||||
p.sendAlert("PlayerVolume", err.Error(), Warning)
|
||||
return
|
||||
}
|
||||
if err2 != nil {
|
||||
msg = PlayerCrashMessage{err}
|
||||
p.synth = nil
|
||||
p.sendAlert("PlayerVolume", err2.Error(), Warning)
|
||||
return
|
||||
}
|
||||
p.send(msg)
|
||||
p.send(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
// we were not able to fill the buffer with NUM_RENDER_TRIES attempts, destroy synth and throw an error
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synth did not fill the audio buffer even with %d render calls", numRenderTries)})
|
||||
p.sendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
|
||||
}
|
||||
|
||||
func (p *Player) advanceRow() {
|
||||
if p.song.Score.Length == 0 || p.song.Score.RowsPerPattern == 0 {
|
||||
return
|
||||
}
|
||||
p.position.Row++ // advance row (this is why we subtracted one in Play())
|
||||
p.position = p.position.Wrap(p.song.Score)
|
||||
p.songPos.PatternRow++ // advance row (this is why we subtracted one in Play())
|
||||
if p.loop.Length > 0 && p.songPos.PatternRow >= p.song.Score.RowsPerPattern && p.songPos.OrderRow == p.loop.Start+p.loop.Length-1 {
|
||||
p.songPos.PatternRow = 0
|
||||
p.songPos.OrderRow = p.loop.Start
|
||||
}
|
||||
p.songPos = p.song.Score.Wrap(p.songPos)
|
||||
p.send(nil) // just send volume and song row information
|
||||
lastVoice := 0
|
||||
for i, t := range p.song.Score.Tracks {
|
||||
start := lastVoice
|
||||
lastVoice = start + t.NumVoices
|
||||
if p.position.Pattern < 0 || p.position.Pattern >= len(t.Order) {
|
||||
continue
|
||||
}
|
||||
o := t.Order[p.position.Pattern]
|
||||
if o < 0 || o >= len(t.Patterns) {
|
||||
continue
|
||||
}
|
||||
pat := t.Patterns[o]
|
||||
if p.position.Row < 0 || p.position.Row >= len(pat) {
|
||||
continue
|
||||
}
|
||||
n := pat[p.position.Row]
|
||||
n := t.Note(p.songPos)
|
||||
switch {
|
||||
case n == 0:
|
||||
p.releaseTrack(i)
|
||||
@ -245,9 +219,9 @@ func (p *Player) processMessages(context PlayerProcessContext) {
|
||||
loop:
|
||||
for { // process new message
|
||||
select {
|
||||
case msg := <-p.modelMessages:
|
||||
case msg := <-p.modelMsgs:
|
||||
switch m := msg.(type) {
|
||||
case ModelPanicMessage:
|
||||
case PanicMsg:
|
||||
if m.bool {
|
||||
p.synth = nil
|
||||
} else {
|
||||
@ -261,23 +235,25 @@ loop:
|
||||
p.compileOrUpdateSynth()
|
||||
case sointu.Score:
|
||||
p.song.Score = m
|
||||
case ModelPlayingChangedMessage:
|
||||
case Loop:
|
||||
p.loop = m
|
||||
case IsPlayingMsg:
|
||||
p.playing = bool(m.bool)
|
||||
if !p.playing {
|
||||
for i := range p.song.Score.Tracks {
|
||||
p.releaseTrack(i)
|
||||
}
|
||||
}
|
||||
case ModelBPMChangedMessage:
|
||||
case BPMMsg:
|
||||
p.song.BPM = m.int
|
||||
p.compileOrUpdateSynth()
|
||||
case ModelRowsPerBeatChangedMessage:
|
||||
case RowsPerBeatMsg:
|
||||
p.song.RowsPerBeat = m.int
|
||||
p.compileOrUpdateSynth()
|
||||
case ModelPlayFromPositionMessage:
|
||||
case StartPlayMsg:
|
||||
p.playing = true
|
||||
p.position = m.ScoreRow
|
||||
p.position.Row--
|
||||
p.songPos = m.SongPos
|
||||
p.songPos.PatternRow--
|
||||
p.rowtime = math.MaxInt
|
||||
for i, t := range p.song.Score.Tracks {
|
||||
if !t.Effect {
|
||||
@ -285,19 +261,19 @@ loop:
|
||||
p.releaseTrack(i)
|
||||
}
|
||||
}
|
||||
case ModelNoteOnMessage:
|
||||
case NoteOnMsg:
|
||||
if m.IsInstr {
|
||||
p.triggerInstrument(m.Instr, m.Note)
|
||||
} else {
|
||||
p.triggerTrack(m.Track, m.Note)
|
||||
}
|
||||
case ModelNoteOffMessage:
|
||||
case NoteOffMsg:
|
||||
if m.IsInstr {
|
||||
p.releaseInstrument(m.Instr, m.Note)
|
||||
} else {
|
||||
p.releaseTrack(m.Track)
|
||||
}
|
||||
case ModelRecordingMessage:
|
||||
case RecordingMsg:
|
||||
if m.bool {
|
||||
p.recState = recStateWaitingForNote
|
||||
p.recording = Recording{}
|
||||
@ -317,6 +293,15 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) sendAlert(name, message string, priority AlertPriority) {
|
||||
p.send(Alert{
|
||||
Name: name,
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
Duration: defaultAlertDuration,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Player) compileOrUpdateSynth() {
|
||||
if p.song.BPM <= 0 {
|
||||
return // bpm not set yet
|
||||
@ -325,7 +310,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
err := p.synth.Update(p.song.Patch, p.song.BPM)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synth.Update: %w", err)})
|
||||
p.sendAlert("PlayerCrash", fmt.Sprintf("synth.Update: %v", err), Error)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@ -333,7 +318,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
p.synth, err = p.synther.Synth(p.song.Patch, p.song.BPM)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synther.Synth: %w", err)})
|
||||
p.sendAlert("PlayerCrash", fmt.Sprintf("synther.Synth: %v", err), Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -342,7 +327,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
// all sends from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
||||
func (p *Player) send(message interface{}) {
|
||||
select {
|
||||
case p.playerMessages <- PlayerMessage{Panic: p.synth == nil, AverageVolume: p.avgVolumeMeter.Level, PeakVolume: p.peakVolumeMeter.Level, SongRow: p.position, VoiceLevels: p.voiceLevels, Inner: message}:
|
||||
case p.playerMsgs <- PlayerMsg{Panic: p.synth == nil, AverageVolume: p.avgVolumeMeter.Level, PeakVolume: p.peakVolumeMeter.Level, SongPosition: p.songPos, VoiceLevels: p.voiceLevels, Inner: message}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package tracker
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -12,23 +14,31 @@ import (
|
||||
|
||||
//go:generate go run generate/main.go
|
||||
|
||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||
type GmDlsEntry struct {
|
||||
Start int // sample start offset in words
|
||||
LoopStart int // loop start offset in words
|
||||
LoopLength int // loop length in words
|
||||
SuggestedTranspose int // suggested transpose in semitones, so that all samples play at same pitch
|
||||
Name string // sample name
|
||||
}
|
||||
type (
|
||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||
GmDlsEntry struct {
|
||||
Start int // sample start offset in words
|
||||
LoopStart int // loop start offset in words
|
||||
LoopLength int // loop length in words
|
||||
SuggestedTranspose int // suggested transpose in semitones, so that all samples play at same pitch
|
||||
Name string // sample name
|
||||
}
|
||||
|
||||
// GmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
||||
InstrumentPresetYieldFunc func(index int, item string) (ok bool)
|
||||
LoadPreset struct {
|
||||
Index int
|
||||
*Model
|
||||
}
|
||||
)
|
||||
|
||||
// gmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
||||
// GmDlsEntries list based on the sample offset. Do not modify during runtime.
|
||||
var GmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||
|
||||
func init() {
|
||||
for i, e := range GmDlsEntries {
|
||||
key := vm.SampleOffset{Start: uint32(e.Start), LoopStart: uint16(e.LoopStart), LoopLength: uint16(e.LoopLength)}
|
||||
GmDlsEntryMap[key] = i
|
||||
gmDlsEntryMap[key] = i
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +59,7 @@ var defaultUnits = map[string]sointu.Unit{
|
||||
"pan": {Type: "pan", Parameters: map[string]int{"stereo": 0, "panning": 64}},
|
||||
"gain": {Type: "gain", Parameters: map[string]int{"stereo": 0, "gain": 64}},
|
||||
"invgain": {Type: "invgain", Parameters: map[string]int{"stereo": 0, "invgain": 64}},
|
||||
"dbgain": {Type: "dbgain", Parameters: map[string]int{"stereo": 0, "decibels": 64}},
|
||||
"crush": {Type: "crush", Parameters: map[string]int{"stereo": 0, "resolution": 64}},
|
||||
"clip": {Type: "clip", Parameters: map[string]int{"stereo": 0}},
|
||||
"hold": {Type: "hold", Parameters: map[string]int{"stereo": 0, "holdfreq": 64}},
|
||||
@ -102,12 +113,6 @@ var defaultSong = sointu.Song{
|
||||
}}},
|
||||
}
|
||||
|
||||
type delayPreset struct {
|
||||
name string
|
||||
stereo int
|
||||
varArgs []int
|
||||
}
|
||||
|
||||
var reverbs = []delayPreset{
|
||||
{"stereo", 1, []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618,
|
||||
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
||||
@ -116,11 +121,43 @@ var reverbs = []delayPreset{
|
||||
{"right", 0, []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}},
|
||||
}
|
||||
|
||||
type instrumentPresets []sointu.Instrument
|
||||
type delayPreset struct {
|
||||
name string
|
||||
stereo int
|
||||
varArgs []int
|
||||
}
|
||||
|
||||
func (m *Model) IterateInstrumentPresets(yield InstrumentPresetYieldFunc) {
|
||||
for index, instr := range instrumentPresets {
|
||||
if !yield(index, instr.Name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) LoadPreset(index int) Action {
|
||||
return Action{do: func() {
|
||||
defer m.change("LoadPreset", PatchChange, MajorChange)()
|
||||
if m.d.InstrIndex < 0 {
|
||||
m.d.InstrIndex = 0
|
||||
}
|
||||
m.d.InstrIndex2 = m.d.InstrIndex
|
||||
for m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||
}
|
||||
newInstr := instrumentPresets[index].Copy()
|
||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||
}, allowed: func() bool {
|
||||
return true
|
||||
}}
|
||||
}
|
||||
|
||||
type instrumentPresetsSlice []sointu.Instrument
|
||||
|
||||
//go:embed presets/*
|
||||
var instrumentPresetFS embed.FS
|
||||
var InstrumentPresets instrumentPresets
|
||||
var instrumentPresets instrumentPresetsSlice
|
||||
|
||||
func init() {
|
||||
fs.WalkDir(instrumentPresetFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
@ -135,23 +172,34 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if yaml.Unmarshal(data, &instr) != nil {
|
||||
return nil
|
||||
if yaml.Unmarshal(data, &instr) == nil {
|
||||
instrumentPresets = append(instrumentPresets, instr)
|
||||
}
|
||||
InstrumentPresets = append(InstrumentPresets, instr)
|
||||
return nil
|
||||
})
|
||||
sort.Sort(InstrumentPresets)
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
userPresets := filepath.Join(configDir, "sointu", "presets")
|
||||
filepath.WalkDir(userPresets, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if yaml.Unmarshal(data, &instr) == nil {
|
||||
instrumentPresets = append(instrumentPresets, instr)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
sort.Sort(instrumentPresets)
|
||||
}
|
||||
|
||||
func (p instrumentPresets) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
func (p instrumentPresets) Less(i, j int) bool {
|
||||
return p[i].Name < p[j].Name
|
||||
}
|
||||
|
||||
func (p instrumentPresets) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
func (p instrumentPresetsSlice) Len() int { return len(p) }
|
||||
func (p instrumentPresetsSlice) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
||||
func (p instrumentPresetsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
@ -22,9 +22,9 @@ type recordingNote struct {
|
||||
|
||||
var ErrInvalidRows = errors.New("rows per beat and rows per pattern must be greater than 1")
|
||||
|
||||
func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Song, error) {
|
||||
func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Score, error) {
|
||||
if rowsPerBeat <= 1 || rowsPerPattern <= 1 {
|
||||
return sointu.Song{}, ErrInvalidRows
|
||||
return sointu.Score{}, ErrInvalidRows
|
||||
}
|
||||
channelNotes := make([][]recordingNote, 0)
|
||||
// find the length of each note and assign it to its respective channel
|
||||
@ -77,6 +77,12 @@ func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern
|
||||
tracks[i][oldestIndex] = append(tracks[i][oldestIndex], n)
|
||||
}
|
||||
}
|
||||
// if there was tracks that had no notes, create empty tracks for them
|
||||
for i := range channelNotes {
|
||||
if l := len(tracks[i]); l == 0 && l < patch[i].NumVoices {
|
||||
tracks[i] = append(tracks[i], []recordingNote{})
|
||||
}
|
||||
}
|
||||
songLengthPatterns := (frameToRow(recording.BPM, rowsPerBeat, recording.TotalFrames) + rowsPerPattern - 1) / rowsPerPattern
|
||||
songLengthRows := songLengthPatterns * rowsPerPattern
|
||||
songTracks := make([]sointu.Track, 0)
|
||||
@ -88,15 +94,18 @@ func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern
|
||||
flatPattern[k] = 1 // set all notes as holds at first
|
||||
}
|
||||
for _, n := range t {
|
||||
flatPattern.Set(n.startRow, n.note)
|
||||
if n.startRow >= songLengthRows {
|
||||
continue
|
||||
}
|
||||
flatPattern[n.startRow] = n.note
|
||||
if n.endRow < songLengthRows {
|
||||
for l := n.startRow + 1; l < n.endRow; l++ {
|
||||
flatPattern.Set(l, 1)
|
||||
flatPattern[l] = 1
|
||||
}
|
||||
flatPattern.Set(n.endRow, 0)
|
||||
flatPattern[n.endRow] = 0
|
||||
} else {
|
||||
for l := n.startRow + 1; l < songLengthRows; l++ {
|
||||
flatPattern.Set(l, 1)
|
||||
flatPattern[l] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +145,7 @@ func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern
|
||||
}
|
||||
}
|
||||
score := sointu.Score{Length: songLengthPatterns, RowsPerPattern: rowsPerPattern, Tracks: songTracks}
|
||||
return sointu.Song{BPM: int(recording.BPM + 0.5), RowsPerBeat: rowsPerBeat, Score: score, Patch: patch.Copy()}, nil
|
||||
return score, nil
|
||||
}
|
||||
|
||||
func frameToRow(BPM float64, rowsPerBeat, frame int) int {
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import "github.com/vsariola/sointu"
|
||||
|
||||
type (
|
||||
// ScoreRow identifies a row of the song score.
|
||||
ScoreRow struct {
|
||||
Pattern int
|
||||
Row int
|
||||
}
|
||||
|
||||
// ScorePoint identifies a row and a track in a song score.
|
||||
ScorePoint struct {
|
||||
Track int
|
||||
ScoreRow
|
||||
}
|
||||
|
||||
// ScoreRect identifies a rectangular area in a song score.
|
||||
ScoreRect struct {
|
||||
Corner1 ScorePoint
|
||||
Corner2 ScorePoint
|
||||
}
|
||||
)
|
||||
|
||||
func (r ScoreRow) AddRows(rows int) ScoreRow {
|
||||
return ScoreRow{Row: r.Row + rows, Pattern: r.Pattern}
|
||||
}
|
||||
|
||||
func (r ScoreRow) AddPatterns(patterns int) ScoreRow {
|
||||
return ScoreRow{Row: r.Row, Pattern: r.Pattern + patterns}
|
||||
}
|
||||
|
||||
func (r ScoreRow) Wrap(score sointu.Score) ScoreRow {
|
||||
totalRow := r.Pattern*score.RowsPerPattern + r.Row
|
||||
r.Row = mod(totalRow, score.RowsPerPattern)
|
||||
r.Pattern = mod((totalRow-r.Row)/score.RowsPerPattern, score.Length)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ScoreRow) Clamp(score sointu.Score) ScoreRow {
|
||||
totalRow := r.Pattern*score.RowsPerPattern + r.Row
|
||||
if totalRow < 0 {
|
||||
totalRow = 0
|
||||
}
|
||||
if totalRow >= score.LengthInRows() {
|
||||
totalRow = score.LengthInRows() - 1
|
||||
}
|
||||
r.Row = totalRow % score.RowsPerPattern
|
||||
r.Pattern = ((totalRow - r.Row) / score.RowsPerPattern) % score.Length
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ScorePoint) AddRows(rows int) ScorePoint {
|
||||
return ScorePoint{Track: r.Track, ScoreRow: r.ScoreRow.AddRows(rows)}
|
||||
}
|
||||
|
||||
func (r ScorePoint) AddPatterns(patterns int) ScorePoint {
|
||||
return ScorePoint{Track: r.Track, ScoreRow: r.ScoreRow.AddPatterns(patterns)}
|
||||
}
|
||||
|
||||
func (p ScorePoint) Wrap(score sointu.Score) ScorePoint {
|
||||
p.Track = mod(p.Track, len(score.Tracks))
|
||||
p.ScoreRow = p.ScoreRow.Wrap(score)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p ScorePoint) Clamp(score sointu.Score) ScorePoint {
|
||||
if p.Track < 0 {
|
||||
p.Track = 0
|
||||
} else if l := len(score.Tracks); p.Track >= l {
|
||||
p.Track = l - 1
|
||||
}
|
||||
p.ScoreRow = p.ScoreRow.Clamp(score)
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *ScoreRect) Contains(p ScorePoint) bool {
|
||||
track1, track2 := r.Corner1.Track, r.Corner2.Track
|
||||
if track2 < track1 {
|
||||
track1, track2 = track2, track1
|
||||
}
|
||||
if p.Track < track1 || p.Track > track2 {
|
||||
return false
|
||||
}
|
||||
pattern1, row1, pattern2, row2 := r.Corner1.Pattern, r.Corner1.Row, r.Corner2.Pattern, r.Corner2.Row
|
||||
if pattern2 < pattern1 || (pattern1 == pattern2 && row2 < row1) {
|
||||
pattern1, row1, pattern2, row2 = pattern2, row2, pattern1, row1
|
||||
}
|
||||
if p.Pattern < pattern1 || p.Pattern > pattern2 {
|
||||
return false
|
||||
}
|
||||
if p.Pattern == pattern1 && p.Row < row1 {
|
||||
return false
|
||||
}
|
||||
if p.Pattern == pattern2 && p.Row > row2 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
if a < 0 {
|
||||
return b - 1 - mod(-a-1, b)
|
||||
}
|
||||
return a % b
|
||||
}
|
||||
97
tracker/string.go
Normal file
97
tracker/string.go
Normal file
@ -0,0 +1,97 @@
|
||||
package tracker
|
||||
|
||||
type (
|
||||
String struct {
|
||||
StringData
|
||||
}
|
||||
|
||||
StringData interface {
|
||||
Value() string
|
||||
setValue(string)
|
||||
change(kind string) func()
|
||||
}
|
||||
|
||||
FilePath Model
|
||||
InstrumentName Model
|
||||
InstrumentComment Model
|
||||
UnitSearch Model
|
||||
)
|
||||
|
||||
func (v String) Set(value string) {
|
||||
if v.Value() != value {
|
||||
defer v.change("Set")()
|
||||
v.setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) FilePath() *FilePath { return (*FilePath)(m) }
|
||||
func (m *Model) InstrumentName() *InstrumentName { return (*InstrumentName)(m) }
|
||||
func (m *Model) InstrumentComment() *InstrumentComment { return (*InstrumentComment)(m) }
|
||||
func (m *Model) UnitSearch() *UnitSearch { return (*UnitSearch)(m) }
|
||||
|
||||
// FilePathString
|
||||
|
||||
func (v *FilePath) String() String { return String{v} }
|
||||
func (v *FilePath) Value() string { return v.d.FilePath }
|
||||
func (v *FilePath) setValue(value string) { v.d.FilePath = value }
|
||||
func (v *FilePath) change(kind string) func() { return func() {} }
|
||||
|
||||
// UnitSearchString
|
||||
|
||||
func (v *UnitSearch) String() String { return String{v} }
|
||||
func (v *UnitSearch) Value() string { return v.d.UnitSearchString }
|
||||
func (v *UnitSearch) setValue(value string) {
|
||||
v.d.UnitSearchString = value
|
||||
v.d.UnitSearching = true
|
||||
}
|
||||
func (v *UnitSearch) change(kind string) func() { return func() {} }
|
||||
|
||||
// InstrumentNameString
|
||||
|
||||
func (v *InstrumentName) String() String {
|
||||
return String{v}
|
||||
}
|
||||
|
||||
func (v *InstrumentName) Value() string {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return ""
|
||||
}
|
||||
return v.d.Song.Patch[v.d.InstrIndex].Name
|
||||
}
|
||||
|
||||
func (v *InstrumentName) setValue(value string) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].Name = value
|
||||
}
|
||||
|
||||
func (v *InstrumentName) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentNameString."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
|
||||
// InstrumentComment
|
||||
|
||||
func (v *InstrumentComment) String() String {
|
||||
return String{v}
|
||||
}
|
||||
|
||||
func (v *InstrumentComment) Value() string {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return ""
|
||||
}
|
||||
return v.d.Song.Patch[v.d.InstrIndex].Comment
|
||||
}
|
||||
|
||||
func (v *InstrumentComment) setValue(value string) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].Comment = value
|
||||
}
|
||||
|
||||
func (v *InstrumentComment) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentComment."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
632
tracker/table.go
Normal file
632
tracker/table.go
Normal file
@ -0,0 +1,632 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type (
|
||||
Table struct {
|
||||
TableData
|
||||
}
|
||||
|
||||
TableData interface {
|
||||
Cursor() Point
|
||||
Cursor2() Point
|
||||
SetCursor(Point)
|
||||
SetCursor2(Point)
|
||||
Width() int
|
||||
Height() int
|
||||
MoveCursor(dx, dy int) (ok bool)
|
||||
|
||||
clear(p Point)
|
||||
set(p Point, value int)
|
||||
add(rect Rect, delta int) (ok bool)
|
||||
marshal(rect Rect) (data []byte, ok bool)
|
||||
unmarshalAtCursor(data []byte) (ok bool)
|
||||
unmarshalRange(rect Rect, data []byte) (ok bool)
|
||||
change(kind string, severity ChangeSeverity) func()
|
||||
cancel()
|
||||
}
|
||||
|
||||
Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
Rect struct {
|
||||
TopLeft, BottomRight Point
|
||||
}
|
||||
|
||||
Order Model
|
||||
Notes Model
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Order() *Order { return (*Order)(m) }
|
||||
func (m *Model) Notes() *Notes { return (*Notes)(m) }
|
||||
|
||||
// Rect methods
|
||||
|
||||
func (r *Rect) Contains(p Point) bool {
|
||||
return r.TopLeft.X <= p.X && p.X <= r.BottomRight.X &&
|
||||
r.TopLeft.Y <= p.Y && p.Y <= r.BottomRight.Y
|
||||
}
|
||||
|
||||
func (r *Rect) Width() int {
|
||||
return r.BottomRight.X - r.TopLeft.X + 1
|
||||
}
|
||||
|
||||
func (r *Rect) Height() int {
|
||||
return r.BottomRight.Y - r.TopLeft.Y + 1
|
||||
}
|
||||
|
||||
func (r *Rect) Limit(width, height int) {
|
||||
if r.TopLeft.X < 0 {
|
||||
r.TopLeft.X = 0
|
||||
}
|
||||
if r.TopLeft.Y < 0 {
|
||||
r.TopLeft.Y = 0
|
||||
}
|
||||
if r.BottomRight.X >= width {
|
||||
r.BottomRight.X = width - 1
|
||||
}
|
||||
if r.BottomRight.Y >= height {
|
||||
r.BottomRight.Y = height - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Table methods
|
||||
|
||||
func (v Table) Range() (rect Rect) {
|
||||
rect.TopLeft.X = intMin(v.Cursor().X, v.Cursor2().X)
|
||||
rect.TopLeft.Y = intMin(v.Cursor().Y, v.Cursor2().Y)
|
||||
rect.BottomRight.X = intMax(v.Cursor().X, v.Cursor2().X)
|
||||
rect.BottomRight.Y = intMax(v.Cursor().Y, v.Cursor2().Y)
|
||||
return
|
||||
}
|
||||
|
||||
func (v Table) Copy() ([]byte, bool) {
|
||||
ret, ok := v.marshal(v.Range())
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func (v Table) Paste(data []byte) bool {
|
||||
defer v.change("Paste", MajorChange)()
|
||||
if v.Cursor() == v.Cursor2() {
|
||||
return v.unmarshalAtCursor(data)
|
||||
} else {
|
||||
return v.unmarshalRange(v.Range(), data)
|
||||
}
|
||||
}
|
||||
|
||||
func (v Table) Clear() {
|
||||
defer v.change("Clear", MajorChange)()
|
||||
rect := v.Range()
|
||||
rect.Limit(v.Width(), v.Height())
|
||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||
v.clear(Point{x, y})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v Table) Fill(value int) {
|
||||
defer v.change("Fill", MajorChange)()
|
||||
rect := v.Range()
|
||||
rect.Limit(v.Width(), v.Height())
|
||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||
v.set(Point{x, y}, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v Table) Add(delta int) {
|
||||
defer v.change("Add", MinorChange)()
|
||||
if !v.add(v.Range(), delta) {
|
||||
v.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (v Table) SetCursorX(x int) {
|
||||
p := v.Cursor()
|
||||
p.X = x
|
||||
v.SetCursor(p)
|
||||
}
|
||||
|
||||
func (v Table) SetCursorY(y int) {
|
||||
p := v.Cursor()
|
||||
p.Y = y
|
||||
v.SetCursor(p)
|
||||
}
|
||||
|
||||
// Order methods
|
||||
|
||||
func (v *Order) Table() Table {
|
||||
return Table{v}
|
||||
}
|
||||
|
||||
func (m *Order) Cursor() Point {
|
||||
t := intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
p := intMax(intMin(m.d.Cursor.OrderRow, m.d.Song.Score.Length-1), 0)
|
||||
return Point{t, p}
|
||||
}
|
||||
|
||||
func (m *Order) Cursor2() Point {
|
||||
t := intMax(intMin(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
p := intMax(intMin(m.d.Cursor2.OrderRow, m.d.Song.Score.Length-1), 0)
|
||||
return Point{t, p}
|
||||
}
|
||||
|
||||
func (m *Order) SetCursor(p Point) {
|
||||
m.d.Cursor.Track = intMax(intMin(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
y := intMax(intMin(p.Y, m.d.Song.Score.Length-1), 0)
|
||||
if y != m.d.Cursor.OrderRow {
|
||||
m.noteTracking = false
|
||||
}
|
||||
m.d.Cursor.OrderRow = y
|
||||
m.updateCursorRows()
|
||||
}
|
||||
|
||||
func (m *Order) SetCursor2(p Point) {
|
||||
m.d.Cursor2.Track = intMax(intMin(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
m.d.Cursor2.OrderRow = intMax(intMin(p.Y, m.d.Song.Score.Length-1), 0)
|
||||
m.updateCursorRows()
|
||||
}
|
||||
|
||||
func (v *Order) updateCursorRows() {
|
||||
if v.Cursor() == v.Cursor2() {
|
||||
v.d.Cursor.PatternRow = 0
|
||||
v.d.Cursor2.PatternRow = 0
|
||||
return
|
||||
}
|
||||
if v.d.Cursor.OrderRow > v.d.Cursor2.OrderRow {
|
||||
v.d.Cursor.PatternRow = v.d.Song.Score.RowsPerPattern - 1
|
||||
v.d.Cursor2.PatternRow = 0
|
||||
} else {
|
||||
v.d.Cursor.PatternRow = 0
|
||||
v.d.Cursor2.PatternRow = v.d.Song.Score.RowsPerPattern - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Order) Width() int {
|
||||
return len((*Model)(v).d.Song.Score.Tracks)
|
||||
}
|
||||
|
||||
func (v *Order) Height() int {
|
||||
return (*Model)(v).d.Song.Score.Length
|
||||
}
|
||||
|
||||
func (v *Order) MoveCursor(dx, dy int) (ok bool) {
|
||||
p := v.Cursor()
|
||||
p.X += dx
|
||||
p.Y += dy
|
||||
v.SetCursor(p)
|
||||
return p == v.Cursor()
|
||||
}
|
||||
|
||||
func (m *Order) clear(p Point) {
|
||||
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, -1)
|
||||
}
|
||||
|
||||
func (m *Order) set(p Point, value int) {
|
||||
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, value)
|
||||
}
|
||||
|
||||
func (v *Order) add(rect Rect, delta int) (ok bool) {
|
||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||
if !v.add1(Point{x, y}, delta) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Order) add1(p Point, delta int) (ok bool) {
|
||||
if p.X < 0 || p.X >= len(v.d.Song.Score.Tracks) {
|
||||
return true
|
||||
}
|
||||
val := v.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
|
||||
if val < 0 {
|
||||
return true
|
||||
}
|
||||
val += delta
|
||||
if val < 0 || val > 36 {
|
||||
return false
|
||||
}
|
||||
v.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
|
||||
return true
|
||||
}
|
||||
|
||||
type marshalOrder struct {
|
||||
Order []int `yaml:",flow"`
|
||||
}
|
||||
|
||||
type marshalTracks struct {
|
||||
Tracks []marshalOrder
|
||||
}
|
||||
|
||||
func (m *Order) marshal(rect Rect) (data []byte, ok bool) {
|
||||
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
||||
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
||||
var table = marshalTracks{Tracks: make([]marshalOrder, 0, width)}
|
||||
for x := 0; x < width; x++ {
|
||||
ax := x + rect.TopLeft.X
|
||||
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
|
||||
continue
|
||||
}
|
||||
table.Tracks = append(table.Tracks, marshalOrder{Order: make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)})
|
||||
for y := 0; y < height; y++ {
|
||||
table.Tracks[x].Order = append(table.Tracks[x].Order, m.d.Song.Score.Tracks[ax].Order.Get(y+rect.TopLeft.Y))
|
||||
}
|
||||
}
|
||||
ret, err := yaml.Marshal(table)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func (m *Order) unmarshal(data []byte) (marshalTracks, bool) {
|
||||
var table marshalTracks
|
||||
yaml.Unmarshal(data, &table)
|
||||
if len(table.Tracks) == 0 {
|
||||
return marshalTracks{}, false
|
||||
}
|
||||
for i := 0; i < len(table.Tracks); i++ {
|
||||
if len(table.Tracks[i].Order) > 0 {
|
||||
return table, true
|
||||
}
|
||||
}
|
||||
return marshalTracks{}, false
|
||||
}
|
||||
|
||||
func (v *Order) unmarshalAtCursor(data []byte) bool {
|
||||
table, ok := v.unmarshal(data)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(table.Tracks); i++ {
|
||||
for j, q := range table.Tracks[i].Order {
|
||||
if table.Tracks[i].Order[j] < -1 || table.Tracks[i].Order[j] > 36 {
|
||||
continue
|
||||
}
|
||||
x := i + v.Cursor().X
|
||||
y := j + v.Cursor().Y
|
||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
|
||||
continue
|
||||
}
|
||||
v.d.Song.Score.Tracks[x].Order.Set(y, q)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Order) unmarshalRange(rect Rect, data []byte) bool {
|
||||
table, ok := v.unmarshal(data)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < rect.Width(); i++ {
|
||||
for j := 0; j < rect.Height(); j++ {
|
||||
k := i % len(table.Tracks)
|
||||
l := j % len(table.Tracks[k].Order)
|
||||
a := table.Tracks[k].Order[l]
|
||||
if a < -1 || a > 36 {
|
||||
continue
|
||||
}
|
||||
x := i + rect.TopLeft.X
|
||||
y := j + rect.TopLeft.Y
|
||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
|
||||
continue
|
||||
}
|
||||
v.d.Song.Score.Tracks[x].Order.Set(y, a)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Order) change(kind string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *Order) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (m *Order) Value(p Point) int {
|
||||
if p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
||||
return -1
|
||||
}
|
||||
return m.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
|
||||
}
|
||||
|
||||
func (m *Order) SetValue(p Point, val int) {
|
||||
defer (*Model)(m).change("OrderElement.SetValue", ScoreChange, MinorChange)()
|
||||
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
|
||||
}
|
||||
|
||||
func (e *Order) Title(x int) (title string) {
|
||||
title = "?"
|
||||
if x < 0 || x >= len(e.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
t := e.d.Song.Score.Tracks[x]
|
||||
firstVoice := e.d.Song.Score.FirstVoiceForTrack(x)
|
||||
lastVoice := firstVoice + t.NumVoices - 1
|
||||
firstIndex, err := e.d.Song.Patch.InstrumentForVoice(firstVoice)
|
||||
lastIndex, err2 := e.d.Song.Patch.InstrumentForVoice(lastVoice)
|
||||
if err != nil || err2 != nil {
|
||||
return
|
||||
}
|
||||
switch diff := lastIndex - firstIndex; diff {
|
||||
case 0:
|
||||
title = e.d.Song.Patch[firstIndex].Name
|
||||
default:
|
||||
n1 := e.d.Song.Patch[firstIndex].Name
|
||||
n2 := e.d.Song.Patch[firstIndex+1].Name
|
||||
if len(n1) > 0 {
|
||||
n1 = string(n1[0])
|
||||
} else {
|
||||
n1 = "?"
|
||||
}
|
||||
if len(n2) > 0 {
|
||||
n2 = string(n2[0])
|
||||
} else {
|
||||
n2 = "?"
|
||||
}
|
||||
if diff > 1 {
|
||||
title = n1 + "/" + n2 + "..."
|
||||
} else {
|
||||
title = n1 + "/" + n2
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NoteTable
|
||||
|
||||
func (v *Notes) Table() Table {
|
||||
return Table{v}
|
||||
}
|
||||
|
||||
func (m *Notes) Cursor() Point {
|
||||
t := intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
p := intMax(intMin(m.d.Song.Score.SongRow(m.d.Cursor.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
|
||||
return Point{t, p}
|
||||
}
|
||||
|
||||
func (m *Notes) Cursor2() Point {
|
||||
t := intMax(intMin(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
p := intMax(intMin(m.d.Song.Score.SongRow(m.d.Cursor2.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
|
||||
return Point{t, p}
|
||||
}
|
||||
|
||||
func (v *Notes) SetCursor(p Point) {
|
||||
v.d.Cursor.Track = intMax(intMin(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
||||
newPos := v.d.Song.Score.Wrap(sointu.SongPos{PatternRow: p.Y})
|
||||
if newPos != v.d.Cursor.SongPos {
|
||||
v.noteTracking = false
|
||||
}
|
||||
v.d.Cursor.SongPos = newPos
|
||||
}
|
||||
|
||||
func (v *Notes) SetCursor2(p Point) {
|
||||
v.d.Cursor2.Track = intMax(intMin(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
||||
v.d.Cursor2.SongPos = v.d.Song.Score.Wrap(sointu.SongPos{PatternRow: p.Y})
|
||||
}
|
||||
|
||||
func (v *Notes) Width() int {
|
||||
return len((*Model)(v).d.Song.Score.Tracks)
|
||||
}
|
||||
|
||||
func (v *Notes) Height() int {
|
||||
return (*Model)(v).d.Song.Score.Length * (*Model)(v).d.Song.Score.RowsPerPattern
|
||||
}
|
||||
|
||||
func (v *Notes) MoveCursor(dx, dy int) (ok bool) {
|
||||
p := v.Cursor()
|
||||
for dx < 0 {
|
||||
if v.Effect(p.X) && v.d.LowNibble {
|
||||
v.d.LowNibble = false
|
||||
} else {
|
||||
p.X--
|
||||
v.d.LowNibble = true
|
||||
}
|
||||
dx++
|
||||
}
|
||||
for dx > 0 {
|
||||
if v.Effect(p.X) && !v.d.LowNibble {
|
||||
v.d.LowNibble = true
|
||||
} else {
|
||||
p.X++
|
||||
v.d.LowNibble = false
|
||||
}
|
||||
dx--
|
||||
}
|
||||
p.Y += dy
|
||||
v.SetCursor(p)
|
||||
return p == v.Cursor()
|
||||
}
|
||||
|
||||
func (v *Notes) clear(p Point) {
|
||||
v.SetValue(p, 1)
|
||||
}
|
||||
|
||||
func (v *Notes) set(p Point, value int) {
|
||||
v.SetValue(p, byte(value))
|
||||
}
|
||||
|
||||
func (v *Notes) add(rect Rect, delta int) (ok bool) {
|
||||
for x := rect.BottomRight.X; x >= rect.TopLeft.X; x-- {
|
||||
for y := rect.BottomRight.Y; y >= rect.TopLeft.Y; y-- {
|
||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
||||
continue
|
||||
}
|
||||
pos := v.d.Song.Score.SongPos(y)
|
||||
note := v.d.Song.Score.Tracks[x].Note(pos)
|
||||
if note <= 1 {
|
||||
continue
|
||||
}
|
||||
newVal := int(note) + delta
|
||||
if newVal < 2 {
|
||||
newVal = 2
|
||||
} else if newVal > 255 {
|
||||
newVal = 255
|
||||
}
|
||||
// only do all sets after all gets, so we don't accidentally adjust single note multiple times
|
||||
defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type noteTable struct {
|
||||
Notes [][]byte `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (m *Notes) marshal(rect Rect) (data []byte, ok bool) {
|
||||
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
||||
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
||||
var table = noteTable{Notes: make([][]byte, 0, width)}
|
||||
for x := 0; x < width; x++ {
|
||||
table.Notes = append(table.Notes, make([]byte, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
|
||||
for y := 0; y < height; y++ {
|
||||
pos := m.d.Song.Score.SongPos(y + rect.TopLeft.Y)
|
||||
ax := x + rect.TopLeft.X
|
||||
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
|
||||
continue
|
||||
}
|
||||
table.Notes[x] = append(table.Notes[x], m.d.Song.Score.Tracks[ax].Note(pos))
|
||||
}
|
||||
}
|
||||
ret, err := yaml.Marshal(table)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func (v *Notes) unmarshal(data []byte) (noteTable, bool) {
|
||||
var table noteTable
|
||||
yaml.Unmarshal(data, &table)
|
||||
if len(table.Notes) == 0 {
|
||||
return noteTable{}, false
|
||||
}
|
||||
for i := 0; i < len(table.Notes); i++ {
|
||||
if len(table.Notes[i]) > 0 {
|
||||
return table, true
|
||||
}
|
||||
}
|
||||
return noteTable{}, false
|
||||
}
|
||||
|
||||
func (v *Notes) unmarshalAtCursor(data []byte) bool {
|
||||
table, ok := v.unmarshal(data)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(table.Notes); i++ {
|
||||
for j, q := range table.Notes[i] {
|
||||
x := i + v.Cursor().X
|
||||
y := j + v.Cursor().Y
|
||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
||||
continue
|
||||
}
|
||||
pos := v.d.Song.Score.SongPos(y)
|
||||
v.d.Song.Score.Tracks[x].SetNote(pos, q)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Notes) unmarshalRange(rect Rect, data []byte) bool {
|
||||
table, ok := v.unmarshal(data)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < rect.Width(); i++ {
|
||||
for j := 0; j < rect.Height(); j++ {
|
||||
k := i % len(table.Notes)
|
||||
l := j % len(table.Notes[k])
|
||||
a := table.Notes[k][l]
|
||||
x := i + rect.TopLeft.X
|
||||
y := j + rect.TopLeft.Y
|
||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
||||
continue
|
||||
}
|
||||
pos := v.d.Song.Score.SongPos(y)
|
||||
v.d.Song.Score.Tracks[x].SetNote(pos, a)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Notes) change(kind string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *Notes) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (m *Notes) Value(p Point) byte {
|
||||
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
||||
return 1
|
||||
}
|
||||
pos := m.d.Song.Score.SongPos(p.Y)
|
||||
return m.d.Song.Score.Tracks[p.X].Note(pos)
|
||||
}
|
||||
|
||||
func (m *Notes) Effect(x int) bool {
|
||||
if x < 0 || x >= len(m.d.Song.Score.Tracks) {
|
||||
return false
|
||||
}
|
||||
return m.d.Song.Score.Tracks[x].Effect
|
||||
}
|
||||
|
||||
func (m *Notes) LowNibble() bool {
|
||||
return m.d.LowNibble
|
||||
}
|
||||
|
||||
func (m *Notes) Unique(t, p int) bool {
|
||||
if t < 0 || t >= len(m.cachePatternUseCount) || p < 0 || p >= len(m.cachePatternUseCount[t]) {
|
||||
return false
|
||||
}
|
||||
return m.cachePatternUseCount[t][p] == 1
|
||||
}
|
||||
|
||||
func (m *Notes) SetValue(p Point, val byte) {
|
||||
defer m.change("SetValue", MinorChange)()
|
||||
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
track := &(m.d.Song.Score.Tracks[p.X])
|
||||
pos := m.d.Song.Score.SongPos(p.Y)
|
||||
(*track).SetNote(pos, val)
|
||||
}
|
||||
|
||||
func (v *Notes) FillNibble(value byte, lowNibble bool) {
|
||||
defer v.change("FillNibble", MajorChange)()
|
||||
rect := Table{v}.Range()
|
||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||
val := v.Value(Point{x, y})
|
||||
if val == 1 {
|
||||
val = 0 // treat hold also as 0
|
||||
}
|
||||
if lowNibble {
|
||||
val = (val & 0xf0) | byte(value&15)
|
||||
} else {
|
||||
val = (val & 0x0f) | byte((value&15)<<4)
|
||||
}
|
||||
v.SetValue(Point{x, y}, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,7 +78,7 @@ func NewBytecode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*Bytecode,
|
||||
return nil, errors.New("Each instrument must have at least 1 voice")
|
||||
}
|
||||
for unitIndex, unit := range instr.Units {
|
||||
if unit.Type == "" { // empty units are just ignored & skipped
|
||||
if unit.Type == "" || unit.Disabled { // empty units are just ignored & skipped
|
||||
continue
|
||||
}
|
||||
opcode, ok := featureSet.Opcode(unit.Type)
|
||||
|
||||
@ -40,7 +40,7 @@ func TestOscillatSine(t *testing.T) {
|
||||
}}}
|
||||
tracks := []sointu.Track{{NumVoices: 1, Order: []int{0}, Patterns: []sointu.Pattern{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}}}
|
||||
song := sointu.Song{BPM: 100, RowsPerBeat: 4, Score: sointu.Score{RowsPerPattern: 16, Length: 1, Tracks: tracks}, Patch: patch}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song)
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Render failed: %v", err)
|
||||
}
|
||||
@ -95,7 +95,7 @@ func TestAllRegressionTests(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse the .yml file: %v", err)
|
||||
}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song)
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil)
|
||||
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()] // extend to the nominal length always.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
|
||||
@ -14,13 +14,8 @@ func flattenSequence(t sointu.Track, songLength int, rowsPerPattern int, release
|
||||
notes := make([]int, sumLen)
|
||||
k := 0
|
||||
for i := 0; i < songLength; i++ {
|
||||
patIndex := t.Order.Get(i)
|
||||
var pattern sointu.Pattern = nil
|
||||
if patIndex >= 0 && patIndex < len(t.Patterns) {
|
||||
pattern = t.Patterns[patIndex]
|
||||
}
|
||||
for j := 0; j < rowsPerPattern; j++ {
|
||||
note := int(pattern.Get(j))
|
||||
note := int(t.Note(sointu.SongPos{OrderRow: i, PatternRow: j}))
|
||||
if releaseFirst && i == 0 && j == 0 && note == 1 {
|
||||
note = 0
|
||||
}
|
||||
|
||||
@ -98,6 +98,41 @@ su_op_invgain_mono:
|
||||
{{- end}}
|
||||
{{end}}
|
||||
|
||||
{{- if .HasOp "dbgain"}}
|
||||
;-------------------------------------------------------------------------------
|
||||
; DBGAIN opcode: apply gain on the signal, with gain given in decibels
|
||||
;-------------------------------------------------------------------------------
|
||||
; Mono: x -> x*g, where g = 2**((2*d-1)*6.643856189774724) i.e. -40dB to 40dB, d=[0..1]
|
||||
; Stereo: l r -> l*g r*g
|
||||
;-------------------------------------------------------------------------------
|
||||
{{.Func "su_op_dbgain" "Opcode"}}
|
||||
{{- if .Stereo "dbgain"}}
|
||||
fld dword [{{.Input "dbgain" "decibels"}}] ; d l r
|
||||
{{- .Prepare (.Float 0.5)}}
|
||||
fsub dword [{{.Use (.Float 0.5)}}] ; d-.5
|
||||
fadd st0, st0 ; 2*d-1
|
||||
{{- .Prepare (.Float 6.643856189774724)}}
|
||||
fmul dword [{{.Use (.Float 6.643856189774724)}}] ; (2*d-1)*6.643856189774724
|
||||
{{.Call "su_power"}}
|
||||
{{- if .Mono "dbgain"}}
|
||||
jnc su_op_dbgain_mono
|
||||
{{- end}}
|
||||
fmul st2, st0 ; g l r/g
|
||||
su_op_dbgain_mono:
|
||||
fmulp st1, st0 ; l/g (r/)
|
||||
ret
|
||||
{{- else}}
|
||||
fld dword [{{.Input "dbgain" "decibels"}}] ; d l
|
||||
{{- .Prepare (.Float 0.5)}}
|
||||
fsub dword [{{.Use (.Float 0.5)}}] ; d-.5
|
||||
fadd st0, st0 ; 2*d-1
|
||||
{{- .Prepare (.Float 6.643856189774724)}}
|
||||
fmul dword [{{.Use (.Float 6.643856189774724)}}] ; (2*d-1)*6.643856189774724
|
||||
{{.Call "su_power"}}
|
||||
fmulp st1, st0
|
||||
ret
|
||||
{{- end}}
|
||||
{{end}}
|
||||
|
||||
{{- if .HasOp "filter"}}
|
||||
;-------------------------------------------------------------------------------
|
||||
@ -121,6 +156,9 @@ su_op_invgain_mono:
|
||||
fsubp st2, st0 ; r x-l'
|
||||
fmul dword [{{.WRK}}+8] ; r*b x-l'
|
||||
fsubp st1, st0 ; x-l'-r*b
|
||||
{{- .Float 0.5 | .Prepare | indent 4}}
|
||||
fadd dword [{{.Float 0.5 | .Use}}] ; add and sub small offset to prevent denormalization
|
||||
fsub dword [{{.Float 0.5 | .Use}}] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
|
||||
fst dword [{{.WRK}}+4] ; h'=x-l'-r*b
|
||||
fmul dword [{{.WRK}}+12] ; f2*h'
|
||||
fadd dword [{{.WRK}}+8] ; f2*h'+b
|
||||
|
||||
@ -23,15 +23,17 @@
|
||||
add rsp, 40 ; shadow space, as required by Win64 ABI
|
||||
ret
|
||||
{{else}}
|
||||
mov ebx, su_sample_table
|
||||
push 0 ; OF_READ
|
||||
push ebx ; &ofstruct, blatantly reuse the sample table
|
||||
push su_gmdls_path1 ; path
|
||||
call dword [__imp__OpenFile@12]; eax = OpenFile(path,&ofstruct,OF_READ) // should not touch ebx according to calling convention
|
||||
mov eax, su_sample_table
|
||||
; these are the arguments for ReadFile
|
||||
push 0 ; NULL
|
||||
push ebx ; &bytes_read, reusing sample table again; it does not matter that the first four bytes are trashed
|
||||
push eax ; &bytes_read, reusing sample table again; it does not matter that the first four bytes are trashed
|
||||
push 3440660 ; number of bytes to read
|
||||
push ebx ; here we actually pass the sample table to readfile
|
||||
push eax ; here we actually pass the sample table to readfile
|
||||
; these are for OpenFile
|
||||
push 0 ; OF_READ
|
||||
push eax ; &ofstruct, blatantly reuse the sample table
|
||||
push su_gmdls_path1 ; path
|
||||
call dword [__imp__OpenFile@12]; eax = OpenFile(path,&ofstruct,OF_READ)
|
||||
push eax ; handle to file
|
||||
call dword [__imp__ReadFile@20] ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
|
||||
ret
|
||||
|
||||
@ -56,7 +56,7 @@ su_render_samples_loop:
|
||||
push {{.DI}}
|
||||
fnstsw [{{.SP}}] ; store the FPU status flag to stack top
|
||||
pop {{.DI}} ; {{.DI}} = FPU status flag
|
||||
and {{.DI}}, 0b0011100001000101 ; mask TOP pointer, stack error, zero divide and in{{.VAL}}id operation
|
||||
and {{.DI}}, 0011100001000101b ; mask TOP pointer, stack error, zero divide and in{{.VAL}}id operation
|
||||
test {{.DI}},{{.DI}} ; all the aforementioned bits should be 0!
|
||||
jne su_render_samples_time_finish ; otherwise, we exit due to error
|
||||
cmp eax, [{{.Stack "RowLength"}}] ; if rowtick >= maxtime
|
||||
|
||||
@ -114,6 +114,7 @@ su_op_advance_finish:
|
||||
; Output: st0 : 2^x
|
||||
;-------------------------------------------------------------------------------
|
||||
{{- if not (.HasCall "su_nonlinear_map")}}{{.SectText "su_power"}}{{end}}
|
||||
{{.Export "su_pow" 0}}
|
||||
su_power:
|
||||
fld1 ; 1 x
|
||||
fld st1 ; x 1 x
|
||||
|
||||
@ -120,7 +120,7 @@ su_update_voices_trackloop:
|
||||
movzx eax, byte [{{.SI}}] ; eax = current pattern
|
||||
imul eax, {{.PatternLength}} ; eax = offset to current pattern data
|
||||
{{- .Prepare "su_patterns" .AX | indent 4}}
|
||||
movzx eax,byte [{{.Use "su_patterns" .AX}},{{.DX}}] ; eax = note
|
||||
movzx eax,byte [{{.Use "su_patterns" .AX}} + {{.DX}}] ; eax = note
|
||||
push {{.DX}} ; Stack: ptrnrow
|
||||
xor edx, edx ; edx=0
|
||||
mov ecx, ebx ; ecx=first voice of the track to be done
|
||||
|
||||
@ -98,6 +98,26 @@
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{- if .HasOp "dbgain"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; DBGAIN opcode: apply gain on the signal, with gain given in decibels
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: x -> x*g, where g = 2**((2*d-1)*6.643856189774724) i.e. -40dB to 40dB, d=[0..1]
|
||||
;; Stereo: l r -> l*g r*g
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_dbgain (param $stereo i32)
|
||||
{{- if .Stereo "dbgain"}}
|
||||
(call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "dbgain") 2}}))
|
||||
{{- end}}
|
||||
(call $input (i32.const {{.InputNumber "dbgain" "decibels"}}))
|
||||
(f32.sub (f32.const 0.5))
|
||||
(f32.mul (f32.const 13.287712379549449))
|
||||
(call $pow2)
|
||||
(f32.mul (call $pop))
|
||||
(call $push)
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "filter"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
@ -293,7 +313,7 @@
|
||||
))
|
||||
{{- end}}
|
||||
{{- if .SupportsModulation "delay" "delaytime"}}
|
||||
(i32.trunc_f32_u (f32.add
|
||||
(i32.trunc_f32_s (f32.add
|
||||
(f32.add
|
||||
(local.get $delayTime)
|
||||
(f32.mul
|
||||
@ -304,7 +324,7 @@
|
||||
(f32.const 0.5)
|
||||
))
|
||||
{{- else}}
|
||||
(i32.trunc_f32_u (f32.add (local.get $delayTime) (f32.const 0.5)))
|
||||
(i32.trunc_f32_s (f32.add (local.get $delayTime) (f32.const 0.5)))
|
||||
{{- end}}
|
||||
{{- else}}
|
||||
(i32.load16_u
|
||||
|
||||
@ -353,6 +353,16 @@ func (p *X86Macros) FmtStack() string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (p *X86Macros) Export(name string, numParams int) string {
|
||||
if !p.Amd64 && p.OS == "windows" {
|
||||
return fmt.Sprintf("global _%[1]v@%[2]v\n_%[1]v@%[2]v:", name, numParams*4)
|
||||
}
|
||||
if p.OS == "darwin" {
|
||||
return fmt.Sprintf("global _%[1]v\n_%[1]v:", name)
|
||||
}
|
||||
return fmt.Sprintf("global %[1]v\n%[1]v:", name)
|
||||
}
|
||||
|
||||
func (p *X86Macros) ExportFunc(name string, params ...string) string {
|
||||
numRegisters := 0 // in 32-bit systems, we use stdcall: everything in stack
|
||||
switch {
|
||||
@ -371,13 +381,7 @@ func (p *X86Macros) ExportFunc(name string, params ...string) string {
|
||||
reverseParams[len(params)-1-i] = param
|
||||
}
|
||||
p.Stacklocs = append(reverseParams, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
|
||||
if !p.Amd64 && p.OS == "windows" {
|
||||
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4)
|
||||
}
|
||||
if p.OS == "darwin" {
|
||||
return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name)
|
||||
}
|
||||
return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name)
|
||||
return fmt.Sprintf("%[1]v\n%[2]v", p.SectText(name), p.Export(name, len(params)))
|
||||
}
|
||||
|
||||
func (p *X86Macros) Input(unit string, port string) (string, error) {
|
||||
|
||||
@ -118,7 +118,7 @@ func NecessaryFeaturesFor(patch sointu.Patch) NecessaryFeatures {
|
||||
features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
|
||||
for instrIndex, instrument := range patch {
|
||||
for _, unit := range instrument.Units {
|
||||
if unit.Type == "" {
|
||||
if unit.Type == "" || unit.Disabled {
|
||||
continue
|
||||
}
|
||||
if _, ok := features.opcodes[unit.Type]; !ok {
|
||||
|
||||
@ -346,6 +346,12 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
||||
stack[l-2] /= params[0]
|
||||
}
|
||||
stack[l-1] /= params[0]
|
||||
case opDbgain:
|
||||
gain := float32(math.Pow(2, float64(params[0]*2-1)*6.643856189774724))
|
||||
if stereo {
|
||||
stack[l-2] *= gain
|
||||
}
|
||||
stack[l-1] *= gain
|
||||
case opClip:
|
||||
if stereo {
|
||||
stack[l-2] = clip(stack[l-2])
|
||||
|
||||
@ -43,7 +43,7 @@ func TestAllRegressionTests(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse the .yml file: %v", err)
|
||||
}
|
||||
buffer, err := sointu.Play(vm.GoSynther{}, song)
|
||||
buffer, err := sointu.Play(vm.GoSynther{}, song, nil)
|
||||
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()] // extend to the nominal length always.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
|
||||
@ -8,30 +8,31 @@ const (
|
||||
opClip = 4
|
||||
opCompressor = 5
|
||||
opCrush = 6
|
||||
opDelay = 7
|
||||
opDistort = 8
|
||||
opEnvelope = 9
|
||||
opFilter = 10
|
||||
opGain = 11
|
||||
opHold = 12
|
||||
opIn = 13
|
||||
opInvgain = 14
|
||||
opLoadnote = 15
|
||||
opLoadval = 16
|
||||
opMul = 17
|
||||
opMulp = 18
|
||||
opNoise = 19
|
||||
opOscillator = 20
|
||||
opOut = 21
|
||||
opOutaux = 22
|
||||
opPan = 23
|
||||
opPop = 24
|
||||
opPush = 25
|
||||
opReceive = 26
|
||||
opSend = 27
|
||||
opSpeed = 28
|
||||
opSync = 29
|
||||
opXch = 30
|
||||
opDbgain = 7
|
||||
opDelay = 8
|
||||
opDistort = 9
|
||||
opEnvelope = 10
|
||||
opFilter = 11
|
||||
opGain = 12
|
||||
opHold = 13
|
||||
opIn = 14
|
||||
opInvgain = 15
|
||||
opLoadnote = 16
|
||||
opLoadval = 17
|
||||
opMul = 18
|
||||
opMulp = 19
|
||||
opNoise = 20
|
||||
opOscillator = 21
|
||||
opOut = 22
|
||||
opOutaux = 23
|
||||
opPan = 24
|
||||
opPop = 25
|
||||
opPush = 26
|
||||
opReceive = 27
|
||||
opSend = 28
|
||||
opSpeed = 29
|
||||
opSync = 30
|
||||
opXch = 31
|
||||
)
|
||||
|
||||
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}
|
||||
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}
|
||||
|
||||
Reference in New Issue
Block a user