diff --git a/.gitignore b/.gitignore
index beb6555..3da01d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@
CS-16.pdx/*
CS-16.pdx
TODO.md
+stub.lua
+CHANGELOG.md
+CS-16.pdx.zip
diff --git a/DEV.md b/DEV.md
new file mode 100644
index 0000000..5f362bb
--- /dev/null
+++ b/DEV.md
@@ -0,0 +1,64 @@
+# develop
+
+## building
+
+Building CS-16 is exactly like how you'd build any other Playdate game (in this case, the command is `pdc src/ CS-16.pdx`).
+
+However, I personally use [`just`](https://github.com/casey/just) to make things a little faster! If you install `just` or already have it installed, the default `just` recipe will build CS-16 and open it in the Playdate Simulator, presuming `PlaydateSimulator` is in `$PATH`.
+
+> note: the `just` recipes are only compatible with Linux or MacOS. the `justfile` may need to be modified if you are on Windows.
+
+> also, if you end up taking a look at the code, sorry in advance if it's a mess XD
+
+## visualizers
+
+### format
+A CS-16 visualizer at its core is just a function that gets called every playdate.update() loop, so you can do pretty much whatever you want with it!
+
+In order for it to be recognized by CS-16, your visualizer code must be either a standalone `.lua` file, or be in a folder by the same name as one of the `.lua` files.
+
+CS-16 requires that a visualizer returns a table with the name of the visualizer, as well as the function that will be called every update. When it is called, CS-16 will pass in a key-value table containing many values which you may find useful when creating a visualizer.
+
+Here's the code for one of my visualizers, called "bumper", as an example:
+
+```lua
+local function bumperUpdate(data)
+ if data.beat then
+ pd.display.setOffset(0, 3)
+ elseif math.floor(data.step) % 8 == 2 then
+ pd.display.setOffset(0, 1)
+ else
+ pd.display.setOffset(0, 0)
+ end
+end
+
+return {"bumper", bumperUpdate}
+```
+
+### building and importing
+Currently, there is no way to directly load Lua code from the Playdate's Data/ directory (most likely for security reasons), so you'll have to rebuild CS-16 with your custom visualizers in the source code.
+
+To do this, clone this repository with `git clone https://github.com/nanobot567/CS-16`. Then, navigate to the `src/` directory, and create a new folder named `visualizers` if it doesn't already exist. Here, simply paste your .lua files (or the folder containing your .lua file) and rebuild CS-16 with `pdc src/ CS-16.pdx` (or if you have [`just`](https://github.com/casey/just) installed, `just`).
+
+### visualizer key-value table data
+
+| key | type | note |
+| -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
+| tempo | number | pattern tempo |
+| step | number | pattern step |
+| rawStep | number | pattern step, not rounded |
+| length | number | pattern length |
+| playing | boolean | true if pattern is currently playing |
+| beat | boolean | true if current step is a beat |
+| tracks | object table | table of `playdate.sound.track`s in the pattern |
+| trackNames | string table | table of the names of the tracks |
+| userTrackNames | string table | table of user-defined track names. if a name has not been set, the value at that track's index will be an empty string ("") |
+| trackSwings | number table | table of swing values for each track |
+| mutedTracks | boolean table | mutedTracks[trackNumber] returns true if the track is currently muted |
+| instruments | object table | table of `playdate.sound.instrument`s |
+| instrumentADSRs | number table table | nested tables contain the attack, decay, sustain and release values in that order for each track |
+| instrumentLegatos | boolean table | legato status for each track |
+| instrumentParams | number table table | tables within contain parameter 1 and 2 values for square wave tracks and TE synth tracks (phase, digital, vosim) |
+| instrumentTransposes | number table | contains the transposition value of each track |
+| settings | key-value table | contains the user's settings. refer to your own `settings.json` file if you don't know what the key to a setting may be! |
+| sequencer | object | CS-16's `playdate.sound.sequencer`* |
diff --git a/LICENSE b/LICENSE
index 9476999..29f33da 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023 Nanobot567
+Copyright (c) 2024 Nanobot567
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MANUAL.md b/MANUAL.md
index 629a1bb..e1fa686 100644
--- a/MANUAL.md
+++ b/MANUAL.md
@@ -1,9 +1,10 @@
# CS-16 manual
-In CS-16, there are three main screens, each of which will be explored in this manual:
+In CS-16, there are four main screens, each of which will be explored in this manual:
1. `pattern`
2. `track`
-3. `song`
+3. `fx`
+4. `song`
You can swap between these screens by pressing `B` until the text shown says `screen`, and then cranking. This "crank mode" menu will be accessed often as you use CS-16.
@@ -33,7 +34,7 @@ To make placing down and editing notes at intervals easier, there is an option i

-To edit the pitch of the note, cycle through the crank mode menu until you reach `pitch`, then crank like you did for `note status`. Changing the track, velocity, and note length are done this same way.
+To edit the pitch of the note, cycle through the crank mode menu until you reach `pitch`, then crank like you did for `note status`. Changing the track, velocity, note length, and swing are done this same way.
You can start or stop the sequence by pressing A.
@@ -43,6 +44,8 @@ If you would like to input notes in real time, there is a `record` function in C
While in record mode, the `A`, `B`, `up`, `down`, `left`, and `right` buttons are mapped to tracks in the song. When pressed, a note is placed at that step in the corresponding track. If quantization is enabled (not 1), then each note you place will be quantized to the nearest multiple of the quantization value.
+> swing is applied when you press `stop record`.
+

### track
@@ -58,6 +61,16 @@ In the system menu, there are two options, `copy` and `cpy mode`. To copy a trac

+#### renaming tracks
+
+To rename one of your tracks, hover over the track you would like to rename, enter the system menu and select `rename`.
+
+To remove your custom track name, enter the `rename` keyboard, delete all of the characters, and press `OK`.
+
+> note: there is a 10-character limit.
+
+
+
#### instrument editor
To edit an instrument, select the track you want in the list. It will be marked with the number and the instrument type. Instrument types can be any of the following:
@@ -99,21 +112,76 @@ Within the file picker, you can enter folders or select a file using `A`, and ex
If you have already selected a sample, however, there will be an extra option in the list: `edit sample`. In the `edit sample` screen, you can trim your samples. Pressing `left` or `right` changes the selected side, and pressing `up` or `down` changes the interval at which you trim the sample using the crank.
-If you are editing a sample that was recorded with the Playdate's microphone, you will see a waveform above the start and end frame locations.
+If you are editing a sample that was recorded with the Playdate's microphone and you have enabled `settings / sampling / save waveforms`, you will see a waveform above the start and end frame locations.

> note: double and triple check your sample before you save it! when you trim it you cannot revert to the original sound.
+### fx
+
+
+
+In the `fx` screen, you can apply punch-in effects to your pattern. Currently there are four effects (ordered clockwise, starting from the top):
+
+1. `TAPE`: high-pass and low-pass filters
+2. `BTC`: bitcrush
+3. `WTR`: low-pass filter
+4. `OVD`: overdrive
+
+While in this screen, press A to activate or deactivate punch-in. The word `ACTV!` will appear at the center of the screen when it's activated. Once activated, the d-pad buttons enable the corresponding effect when pressed, and disable when not.
+
+It is possible to "lock" your button presses! Enable the effects you would like to lock, then switch the crank mode to `lock effect`. Crank clockwise until you see a "lock" icon appear in the bottom left of your screen. To unlock effects, either crank counter-clockwise to unlock all of them, or press the corresponding d-pad button when punch-in is active.
+
+If you would like to change an effect intensity (signified by the decimal number next to the effect), first ensure that punch-in is deactivated. Then, change the crank mode to `effect intensity` and press the d-pad button according to the effect you would like to modify. You'll know you're good to go when there's a box around the effect text. Now just crank until you reach your desired value! Intensities range from 0 (off) to 1 (full effect).
+
+> note: this can be done while an effect is applied, allowing for some pretty fun live performance stuff!
+
### song
-
+
In the `song` screen, you can view and modify your song's global options, such as the tempo and pattern length (these can be modified via the crank). Your song name and author name is displayed at the top.
> IMPORTANT NOTE! currently, the tempo can only be changed by intervals of 7.5 because of a bug in Playdate OS. as soon as a fix is implemented, this message will be deleted.
-Here you can also save and load your songs via the Playdate OS menu. In the menu, you can access and change CS-16 settings, such as dark mode, crank sensitivity, and the name used to sign your saved songs.
+Here you can also save and load your songs via the Playdate OS menu. If you select `load`, you can also perform file operations on your songs, such as renaming, deleting, and cloning. You can press `right` to view file metadata as well.
+
+
+
+In the system menu, you can also access and change CS-16 settings, such as dark mode, crank sensitivity, and the name used to sign your saved songs. A full list of settings is below.
+
+- `general/`
+ - `author` (text value, default anonymous)
+ - `output` (audio output. can be auto, speaker, headset, or speaker and headset)
+ - `crank speed`
+ - `credits`
+- `behavior/`
+ - `play on load` (play pattern immediately on song load)
+ - `stop if sampling` (stop the pattern if you are currently sampling audio)
+ - `tempo edit stop` (stop the pattern when tempo is modified)
+ - `save .wav samples` (alongside .pda audio, save .wav files when sampling)
+ - `crank docked screen` (which screen appears when the crank is docked. `none` disables changing the screen)
+- `recording/` (as in tapping in a pattern)
+ - (button) `button track` (when record is active, this button will correspond to this track)
+ - `quantization` (quantize recording, can be either off [1], every 16th note [2], or every 8th note [4])
+- `sampling/`
+ - `sample format` (16 bit or 8 bit)
+ - `save waveforms` (save waveform images along with audio)
+- `ui/`
+ - `dark mode`
+ - `visualizer` (song screen visualizer options)
+ - `sine wave` (sine wave where tempo is proportional to frequency and pattern length is proportional to amplitude)
+ - `notes` (displays track active statuses)
+ - `stars` (purely decorational, but looks pretty awesome lol)
+ - `--- external ---` (below this are custom visualizers)
+ - `show number / total` (display current crank mode number out of total)
+ - `show note names` (display note names in pattern [C#4, F3, etc.])
+ - `animate scrn move` (animate screen transitions)
+ - `use system font` (use an alternate font)
+ - `show log screens` (display log screens, causes some moderate slowdown at the cost of coolness)
+ - `fx screen vfx` (when an effect is active, apply the corresponding screen visual effect as well)
+ - `50fps` (50fps refresh rate)
## other information
@@ -141,13 +209,22 @@ To manage your songs:
2. navigate to `Data/user.*****.com.nano.cs16/songs/`
3. add, copy, delete, or rename your songs, then eject your playdate when you are done.
+> if you are sharing your songs on the internet, you can set your author name in `settings / general`!!
+
### ways to improve performance
All of these are things you can do to improve CS-16's performance and reduce frame drops.
-- In `settings/ui/`...
- - Disable `visualizer`.
+- In `settings / ui /`...
+ - Disable all `visualizer` elements.
- Disable `show note names`.
- Disable `animate scrn move`.
+ - Disable `fx screen vfx`.
- Enable `50fps`.
- Use lower quality samples (ex. lower bitrate)
+
+### custom visualizers
+
+I have a few custom visualizers in this repository under `visualizers` (and maybe you'll find another one on the internet somewhere??? idk haha). To import these into CS-16, check out the visualizer `building / importing` section in the [DEV document](DEV.md).
+
+> note: by default, imported visualizers are disabled. head to `settings / ui / visualizers` to enable them.
\ No newline at end of file
diff --git a/README.md b/README.md
index 6c1336c..3a3e952 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
# cs-16 (cranky synth 16)
a synthesizer for playdate
-

+

-## [manual](https://github.com/nanobot567/cs-16/blob/main/MANUAL.md) | [license](https://github.com/nanobot567/cs-16/blob/main/LICENSE)
+## [manual](https://github.com/nanobot567/cs-16/blob/main/MANUAL.md) | [develop](https://github.com/nanobot567/cs-16/blob/main/DEV.md) | [license](https://github.com/nanobot567/cs-16/blob/main/LICENSE)
## features
- 16 tracks
@@ -29,7 +29,11 @@ a synthesizer for playdate
*track edit view*
-
+
+
+*fx view*
+
+
*song view*
diff --git a/assets/cycle.gif b/assets/cycle.gif
index d5131d0..96ca8a5 100644
Binary files a/assets/cycle.gif and b/assets/cycle.gif differ
diff --git a/assets/fileops.gif b/assets/fileops.gif
new file mode 100644
index 0000000..7afce0e
Binary files /dev/null and b/assets/fileops.gif differ
diff --git a/assets/fx.png b/assets/fx.png
new file mode 100644
index 0000000..83ef9e9
Binary files /dev/null and b/assets/fx.png differ
diff --git a/assets/mockup.gif b/assets/mockup.gif
new file mode 100644
index 0000000..1d7a98c
Binary files /dev/null and b/assets/mockup.gif differ
diff --git a/assets/pattern.png b/assets/pattern.png
index 3ce960b..ced1684 100644
Binary files a/assets/pattern.png and b/assets/pattern.png differ
diff --git a/assets/patternscreen.gif b/assets/patternscreen.gif
new file mode 100644
index 0000000..2813359
Binary files /dev/null and b/assets/patternscreen.gif differ
diff --git a/assets/rename.gif b/assets/rename.gif
new file mode 100644
index 0000000..d232e98
Binary files /dev/null and b/assets/rename.gif differ
diff --git a/assets/song.png b/assets/song.png
index 3e3f4d3..a2526e3 100644
Binary files a/assets/song.png and b/assets/song.png differ
diff --git a/assets/songscreen.gif b/assets/songscreen.gif
new file mode 100644
index 0000000..39fe337
Binary files /dev/null and b/assets/songscreen.gif differ
diff --git a/assets/track-2.png b/assets/track-2.png
index 0d9c981..8e43d03 100644
Binary files a/assets/track-2.png and b/assets/track-2.png differ
diff --git a/assets/track.png b/assets/track.png
index e92e3c2..ed506ae 100644
Binary files a/assets/track.png and b/assets/track.png differ
diff --git a/example-songs/kinomu-alt/1.pda b/example-songs/kinomu-alt/1.pda
new file mode 100644
index 0000000..bfd719d
Binary files /dev/null and b/example-songs/kinomu-alt/1.pda differ
diff --git a/example-songs/kinomu-alt/2.pda b/example-songs/kinomu-alt/2.pda
new file mode 100644
index 0000000..479c802
Binary files /dev/null and b/example-songs/kinomu-alt/2.pda differ
diff --git a/example-songs/kinomu-alt/3.pda b/example-songs/kinomu-alt/3.pda
new file mode 100644
index 0000000..9b51e5a
Binary files /dev/null and b/example-songs/kinomu-alt/3.pda differ
diff --git a/example-songs/kinomu-alt/4.pda b/example-songs/kinomu-alt/4.pda
new file mode 100644
index 0000000..3111fcb
Binary files /dev/null and b/example-songs/kinomu-alt/4.pda differ
diff --git a/example-songs/kinomu-alt/7.pda b/example-songs/kinomu-alt/7.pda
new file mode 100644
index 0000000..9b40c9b
Binary files /dev/null and b/example-songs/kinomu-alt/7.pda differ
diff --git a/example-songs/kinomu-alt/8.pda b/example-songs/kinomu-alt/8.pda
new file mode 100644
index 0000000..afdd6db
Binary files /dev/null and b/example-songs/kinomu-alt/8.pda differ
diff --git a/example-songs/kinomu-alt/song.json b/example-songs/kinomu-alt/song.json
new file mode 100644
index 0000000..879ab49
--- /dev/null
+++ b/example-songs/kinomu-alt/song.json
@@ -0,0 +1 @@
+[[["smp","smp","smp","smp","tri","squ","smp","smp","sin","squ","saw","tri","nse","poP","poD","poV"],[[{"velocity":1,"length":8,"note":60,"step":8},{"velocity":1,"length":8,"note":60,"step":104},{"velocity":1,"length":8,"note":60,"step":264},{"velocity":1,"length":8,"note":60,"step":360},{"velocity":1,"length":8,"note":60,"step":488},{"velocity":1,"length":8,"note":60,"step":504}],[{"velocity":1,"length":8,"note":60,"step":72},{"velocity":1,"length":8,"note":60,"step":168},{"velocity":1,"length":8,"note":60,"step":328},{"velocity":1,"length":8,"note":60,"step":424}],[{"velocity":1,"length":8,"note":60,"step":40},{"velocity":1,"length":8,"note":60,"step":104},{"velocity":1,"length":8,"note":60,"step":168},{"velocity":1,"length":8,"note":60,"step":296},{"velocity":1,"length":8,"note":60,"step":360},{"velocity":1,"length":8,"note":60,"step":424},{"velocity":1,"length":8,"note":60,"step":440},{"velocity":1,"length":8,"note":60,"step":456}],[{"velocity":1,"length":8,"note":67,"step":40},{"velocity":1,"length":8,"note":62,"step":104},{"velocity":1,"length":8,"note":62,"step":168},{"velocity":1,"length":8,"note":55,"step":232},{"velocity":1,"length":8,"note":67,"step":360},{"velocity":1,"length":8,"note":62,"step":456},{"velocity":1,"length":8,"note":67,"step":488}],[{"velocity":1,"length":256,"note":36,"step":8},{"velocity":1,"length":256,"note":31,"step":264},{"velocity":1,"length":128,"note":43,"step":360},{"velocity":1,"length":8,"note":48,"step":488}],[{"velocity":1,"length":8,"note":60,"step":200},{"velocity":1,"length":8,"note":55,"step":232},{"velocity":1,"length":8,"note":60,"step":264},{"velocity":1,"length":8,"note":67,"step":328},{"velocity":1,"length":8,"note":55,"step":360},{"velocity":1,"length":8,"note":63,"step":392},{"velocity":1,"length":8,"note":62,"step":456}],[{"velocity":1,"length":8,"note":60,"step":8},{"velocity":1,"length":8,"note":60,"step":56},{"velocity":1,"length":8,"note":60,"step":104},{"velocity":1,"length":8,"note":60,"step":136},{"velocity":1,"length":8,"note":60,"step":200},{"velocity":1,"length":8,"note":60,"step":264},{"velocity":1,"length":8,"note":60,"step":312},{"velocity":1,"length":8,"note":60,"step":360},{"velocity":1,"length":8,"note":60,"step":392},{"velocity":1,"length":8,"note":60,"step":456}],[{"velocity":1,"length":8,"note":60,"step":424},{"velocity":1,"length":8,"note":60,"step":440},{"velocity":1,"length":8,"note":60,"step":456}],[],[],[],[],[],[],[],[]],[[0,0,0.80000001192093,0.30000001192093],[0,0,0.80000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,2,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.10000000149012,0.40000000596046],[0,0,0.40000000596046,0.40000000596046],[0,0,0.60000002384186,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046]],[false,false,false,false,true,false,false,false,false,false,false,false,false,false,false,false],[[0,0],[0.5,0],[0,0],[0,0],[0,0],[0.40000000596046,0],[0,0],[0,0],[0,0],[0.5,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],[0,0,0,-3,-3,-3,0,0,0,0,0,0,0,0,0,0],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[0,0,-4,0,0,0,0,0,0,0,0,0,0,0,0,0],["kick","snare","hihat","chord","bass","melody","perc","pewpew","","","","","","","",""]],[12,64],["nanobot567",{"millisecond":530,"weekday":3,"second":5,"minute":59,"hour":22,"year":2024,"day":19,"month":6}]]
\ No newline at end of file
diff --git a/example-songs/kinomu/1.pda b/example-songs/kinomu/1.pda
new file mode 100644
index 0000000..bfd719d
Binary files /dev/null and b/example-songs/kinomu/1.pda differ
diff --git a/example-songs/kinomu/2.pda b/example-songs/kinomu/2.pda
new file mode 100644
index 0000000..479c802
Binary files /dev/null and b/example-songs/kinomu/2.pda differ
diff --git a/example-songs/kinomu/3.pda b/example-songs/kinomu/3.pda
new file mode 100644
index 0000000..9b51e5a
Binary files /dev/null and b/example-songs/kinomu/3.pda differ
diff --git a/example-songs/kinomu/4.pda b/example-songs/kinomu/4.pda
new file mode 100644
index 0000000..3111fcb
Binary files /dev/null and b/example-songs/kinomu/4.pda differ
diff --git a/example-songs/kinomu/7.pda b/example-songs/kinomu/7.pda
new file mode 100644
index 0000000..9b40c9b
Binary files /dev/null and b/example-songs/kinomu/7.pda differ
diff --git a/example-songs/kinomu/8.pda b/example-songs/kinomu/8.pda
new file mode 100644
index 0000000..afdd6db
Binary files /dev/null and b/example-songs/kinomu/8.pda differ
diff --git a/example-songs/kinomu/song.json b/example-songs/kinomu/song.json
new file mode 100644
index 0000000..45ecc2f
--- /dev/null
+++ b/example-songs/kinomu/song.json
@@ -0,0 +1 @@
+[[["smp","smp","smp","smp","tri","squ","smp","smp","sin","squ","saw","tri","nse","poP","poD","poV"],[[{"velocity":1,"note":60,"length":8,"step":8},{"velocity":1,"note":60,"length":8,"step":104},{"velocity":1,"note":60,"length":8,"step":264},{"velocity":1,"note":60,"length":8,"step":360},{"velocity":1,"note":60,"length":8,"step":488},{"velocity":1,"note":60,"length":8,"step":504}],[{"velocity":1,"note":60,"length":8,"step":72},{"velocity":1,"note":60,"length":8,"step":168},{"velocity":1,"note":60,"length":8,"step":328},{"velocity":1,"note":60,"length":8,"step":424}],[{"velocity":1,"note":60,"length":8,"step":40},{"velocity":1,"note":60,"length":8,"step":104},{"velocity":1,"note":60,"length":8,"step":168},{"velocity":1,"note":60,"length":8,"step":296},{"velocity":1,"note":60,"length":8,"step":360},{"velocity":1,"note":60,"length":8,"step":424},{"velocity":1,"note":60,"length":8,"step":440},{"velocity":1,"note":60,"length":8,"step":456}],[{"velocity":1,"note":67,"length":8,"step":40},{"velocity":1,"note":62,"length":8,"step":104},{"velocity":1,"note":62,"length":8,"step":168},{"velocity":1,"note":55,"length":8,"step":232},{"velocity":1,"note":67,"length":8,"step":360},{"velocity":1,"note":62,"length":8,"step":456},{"velocity":1,"note":67,"length":8,"step":488}],[{"velocity":1,"note":36,"length":256,"step":8},{"velocity":1,"note":31,"length":256,"step":264},{"velocity":1,"note":43,"length":128,"step":360},{"velocity":1,"note":48,"length":8,"step":488}],[{"velocity":1,"note":60,"length":8,"step":200},{"velocity":1,"note":55,"length":8,"step":232},{"velocity":1,"note":60,"length":8,"step":264},{"velocity":1,"note":67,"length":8,"step":328},{"velocity":1,"note":55,"length":8,"step":360},{"velocity":1,"note":63,"length":8,"step":392},{"velocity":1,"note":62,"length":8,"step":456}],[{"velocity":1,"note":60,"length":8,"step":8},{"velocity":1,"note":60,"length":8,"step":56},{"velocity":1,"note":60,"length":8,"step":104},{"velocity":1,"note":60,"length":8,"step":136},{"velocity":1,"note":60,"length":8,"step":200},{"velocity":1,"note":60,"length":8,"step":264},{"velocity":1,"note":60,"length":8,"step":312},{"velocity":1,"note":60,"length":8,"step":360},{"velocity":1,"note":60,"length":8,"step":392},{"velocity":1,"note":60,"length":8,"step":456}],[{"velocity":1,"note":60,"length":8,"step":424},{"velocity":1,"note":60,"length":8,"step":440},{"velocity":1,"note":60,"length":8,"step":456}],[],[],[],[],[],[],[],[]],[[0,0,0.80000001192093,0.30000001192093],[0,0,0.80000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,2,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.10000000149012,0.40000000596046],[0,0,0.40000000596046,0.40000000596046],[0,0,0.60000002384186,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046]],[false,false,false,false,true,false,false,false,false,false,false,false,false,false,false,false],[[0,0],[0.5,0],[0,0],[0,0],[0,0],[0.40000000596046,0],[0,0],[0,0],[0,0],[0.5,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[0,0,-4,0,0,0,0,0,0,0,0,0,0,0,0,0],["kick","snare","hihat","chord","bass","melody","perc","pewpew","","","","","","","",""]],[22,64],["nanobot567",{"second":19,"day":15,"hour":15,"minute":18,"millisecond":785,"month":6,"weekday":6,"year":2024}]]
\ No newline at end of file
diff --git a/example-songs/sql_injection/2.pda b/example-songs/sql_injection/2.pda
new file mode 100644
index 0000000..adb0e1b
Binary files /dev/null and b/example-songs/sql_injection/2.pda differ
diff --git a/example-songs/sql_injection/3.pda b/example-songs/sql_injection/3.pda
new file mode 100644
index 0000000..d0c49c8
Binary files /dev/null and b/example-songs/sql_injection/3.pda differ
diff --git a/example-songs/sql_injection/song.json b/example-songs/sql_injection/song.json
new file mode 100644
index 0000000..0ff8187
--- /dev/null
+++ b/example-songs/sql_injection/song.json
@@ -0,0 +1 @@
+[[["sin","smp","smp","tri","nse","saw","poD","poV","sin","squ","saw","tri","nse","poP","poD","poV"],[[{"velocity":1,"length":8,"note":60,"step":8},{"velocity":1,"length":8,"note":60,"step":56},{"velocity":1,"length":8,"note":60,"step":104},{"velocity":1,"length":8,"note":60,"step":136},{"velocity":1,"length":8,"note":63,"step":168},{"velocity":1,"length":8,"note":48,"step":200}],[{"velocity":1,"length":8,"note":60,"step":72},{"velocity":1,"length":8,"note":60,"step":200}],[{"velocity":1,"length":8,"note":60,"step":8},{"velocity":1,"length":8,"note":60,"step":56},{"velocity":1,"length":8,"note":60,"step":104},{"velocity":1,"length":8,"note":60,"step":168},{"velocity":1,"length":8,"note":60,"step":200}],[{"velocity":1,"length":8,"note":60,"step":104},{"velocity":1,"length":8,"note":48,"step":168},{"velocity":1,"length":8,"note":36,"step":200}],[{"velocity":0.30000001192093,"length":24,"note":69,"step":104},{"velocity":1,"length":8,"note":117,"step":232},{"velocity":0.20000000298023,"length":8,"note":81,"step":235},{"velocity":1,"length":8,"note":117,"step":248}],[{"velocity":1,"length":8,"note":48,"step":8},{"velocity":1,"length":8,"note":48,"step":56},{"velocity":1,"length":8,"note":48,"step":104},{"velocity":1,"length":8,"note":48,"step":136},{"velocity":1,"length":8,"note":51,"step":168},{"velocity":1,"length":8,"note":36,"step":200}],[],[],[],[],[],[],[],[],[],[]],[[0,0,0.30000001192093,0.40000000596046],[0,0,0.89999997615814,0.40000000596046],[0,0.80000001192093,0.60000002384186,0.40000000596046],[0.40000000596046,0,0.40000000596046,0.40000000596046],[1.5,0.40000000596046,0,0],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046]],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[[0.10000000149012,0],[0.5,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0.5,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[1,1,1,1,0.5,0.10000000149012,1,1,1,1,1,1,1,1,1,1],[0,0,0,0,-5,0,0,0,0,0,0,0,0,0,0,0],["","crank","kick","","","","","","","","","","","","",""]],[14,32],["nanobot567",{"weekday":6,"hour":18,"minute":1,"second":48,"millisecond":230,"month":6,"year":2024,"day":15}]]
\ No newline at end of file
diff --git a/example-songs/taciturn/1.pda b/example-songs/taciturn/1.pda
new file mode 100644
index 0000000..36199c9
Binary files /dev/null and b/example-songs/taciturn/1.pda differ
diff --git a/example-songs/taciturn/2.pda b/example-songs/taciturn/2.pda
new file mode 100644
index 0000000..44b9d96
Binary files /dev/null and b/example-songs/taciturn/2.pda differ
diff --git a/example-songs/taciturn/5.pda b/example-songs/taciturn/5.pda
new file mode 100644
index 0000000..25040d6
Binary files /dev/null and b/example-songs/taciturn/5.pda differ
diff --git a/example-songs/taciturn/6.pda b/example-songs/taciturn/6.pda
new file mode 100644
index 0000000..c1403da
Binary files /dev/null and b/example-songs/taciturn/6.pda differ
diff --git a/example-songs/taciturn/7.pda b/example-songs/taciturn/7.pda
new file mode 100644
index 0000000..5dafc28
Binary files /dev/null and b/example-songs/taciturn/7.pda differ
diff --git a/example-songs/taciturn/8.pda b/example-songs/taciturn/8.pda
new file mode 100644
index 0000000..92b1018
Binary files /dev/null and b/example-songs/taciturn/8.pda differ
diff --git a/example-songs/taciturn/song.json b/example-songs/taciturn/song.json
new file mode 100644
index 0000000..59b074f
--- /dev/null
+++ b/example-songs/taciturn/song.json
@@ -0,0 +1 @@
+[[["smp","smp","squ","tri","smp","smp","nse","poV","sin","squ","saw","tri","nse","poP","poD","poV"],[[{"step":8,"note":60,"velocity":1,"length":8},{"step":56,"note":60,"velocity":1,"length":8},{"step":104,"note":60,"velocity":1,"length":8},{"step":184,"note":60,"velocity":1,"length":8},{"step":232,"note":60,"velocity":1,"length":8},{"step":264,"note":60,"velocity":1,"length":8},{"step":312,"note":60,"velocity":1,"length":8},{"step":360,"note":60,"velocity":1,"length":8},{"step":440,"note":60,"velocity":1,"length":8},{"step":488,"note":60,"velocity":1,"length":8},{"step":520,"note":60,"velocity":1,"length":8},{"step":568,"note":60,"velocity":1,"length":8},{"step":616,"note":60,"velocity":1,"length":8},{"step":696,"note":60,"velocity":1,"length":8},{"step":744,"note":60,"velocity":1,"length":8},{"step":776,"note":60,"velocity":1,"length":8},{"step":824,"note":60,"velocity":1,"length":8},{"step":872,"note":60,"velocity":1,"length":8},{"step":952,"note":60,"velocity":1,"length":8},{"step":1000,"note":60,"velocity":1,"length":8}],[{"step":136,"note":60,"velocity":1,"length":8},{"step":392,"note":60,"velocity":1,"length":8},{"step":648,"note":60,"velocity":1,"length":8},{"step":904,"note":60,"velocity":1,"length":8}],[{"step":8,"note":84,"velocity":1,"length":8},{"step":72,"note":72,"velocity":1,"length":8},{"step":136,"note":72,"velocity":1,"length":8},{"step":200,"note":72,"velocity":1,"length":8},{"step":264,"note":84,"velocity":1,"length":8},{"step":328,"note":72,"velocity":1,"length":8},{"step":392,"note":72,"velocity":1,"length":8},{"step":456,"note":72,"velocity":1,"length":8},{"step":520,"note":84,"velocity":1,"length":8},{"step":584,"note":72,"velocity":1,"length":8},{"step":648,"note":72,"velocity":1,"length":8},{"step":712,"note":72,"velocity":1,"length":8},{"step":776,"note":84,"velocity":1,"length":8},{"step":840,"note":72,"velocity":1,"length":8},{"step":904,"note":72,"velocity":1,"length":8},{"step":968,"note":72,"velocity":1,"length":8}],[{"step":8,"note":36,"velocity":1,"length":256},{"step":232,"note":48,"velocity":1,"length":48},{"step":264,"note":36,"velocity":1,"length":192},{"step":520,"note":36,"velocity":1,"length":256},{"step":744,"note":48,"velocity":1,"length":48},{"step":776,"note":30,"velocity":1,"length":136},{"step":904,"note":29,"velocity":1,"length":128}],[{"step":8,"note":60,"velocity":1,"length":8},{"step":40,"note":60,"velocity":1,"length":8},{"step":72,"note":60,"velocity":1,"length":8},{"step":104,"note":60,"velocity":1,"length":8},{"step":136,"note":60,"velocity":1,"length":8},{"step":141,"note":60,"velocity":1,"length":8},{"step":168,"note":60,"velocity":1,"length":8},{"step":200,"note":60,"velocity":1,"length":8},{"step":232,"note":60,"velocity":1,"length":8},{"step":264,"note":60,"velocity":1,"length":8},{"step":296,"note":60,"velocity":1,"length":8},{"step":328,"note":60,"velocity":1,"length":8},{"step":360,"note":60,"velocity":1,"length":8},{"step":392,"note":60,"velocity":1,"length":8},{"step":397,"note":60,"velocity":1,"length":8},{"step":424,"note":60,"velocity":1,"length":8},{"step":456,"note":60,"velocity":1,"length":8},{"step":461,"note":60,"velocity":1,"length":8},{"step":488,"note":60,"velocity":1,"length":8},{"step":520,"note":60,"velocity":1,"length":8},{"step":552,"note":60,"velocity":1,"length":8},{"step":584,"note":60,"velocity":1,"length":8},{"step":616,"note":60,"velocity":1,"length":8},{"step":648,"note":60,"velocity":1,"length":8},{"step":653,"note":60,"velocity":1,"length":8},{"step":680,"note":60,"velocity":1,"length":8},{"step":712,"note":60,"velocity":1,"length":8},{"step":744,"note":60,"velocity":1,"length":8},{"step":776,"note":60,"velocity":1,"length":8},{"step":808,"note":60,"velocity":1,"length":8},{"step":840,"note":60,"velocity":1,"length":8},{"step":872,"note":60,"velocity":1,"length":8},{"step":904,"note":60,"velocity":1,"length":8},{"step":936,"note":60,"velocity":1,"length":8},{"step":968,"note":60,"velocity":1,"length":8},{"step":973,"note":60,"velocity":1,"length":8},{"step":1000,"note":60,"velocity":1,"length":8}],[{"step":136,"note":60,"velocity":1,"length":8},{"step":392,"note":60,"velocity":1,"length":8},{"step":648,"note":60,"velocity":1,"length":8},{"step":904,"note":60,"velocity":1,"length":8}],[{"step":456,"note":60,"velocity":1,"length":8},{"step":968,"note":60,"velocity":1,"length":8}],[],[],[],[],[],[],[],[],[]],[[0,0,1.3999999761581,0.40000000596046],[0,0,0.69999998807907,0.40000000596046],[0,0,0.60000002384186,0],[0,0,0.20000000298023,0],[0,0,0.69999998807907,0.40000000596046],[0,0,0.69999998807907,0.40000000596046],[0.40000000596046,0,0,0],[0,0,0.30000001192093,0.40000000596046],[0.40000000596046,0,0,0],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046],[0,0,0.30000001192093,0.40000000596046]],[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],[[0,0],[0.5,0],[1,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0.5,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],[-4,0,0,0,0,0,24,0,0,0,0,0,0,0,0,0],[1,1,0.10000000149012,1,1,1,0.40000000596046,1,1,1,1,1,1,1,1,1],[0,0,0,0,-3,0,0,0,0,0,0,0,0,0,0,0],["kick","snare","metronome","","hat","snare2","","","","","","","","","",""]],[18,128],["nanobot567",{"millisecond":496,"second":37,"minute":26,"month":6,"year":2024,"weekday":7,"hour":13,"day":16}]]
\ No newline at end of file
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..2ac1fd5
--- /dev/null
+++ b/justfile
@@ -0,0 +1,40 @@
+default: build run
+
+[private]
+incrementBuildNumber:
+ #! /bin/python3
+ from sys import argv
+
+ f = open("src/pdxinfo","r")
+ content = f.read()
+ f.close()
+
+ splitnl = content.split("\n")
+ sc = splitnl[5].split("=")
+ buildnum = sc[1]
+
+ splitnl[5] = f"buildNumber={str(int(buildnum)+1)}"
+
+ f = open("src/pdxinfo","w")
+
+ for i in splitnl:
+ if splitnl[len(splitnl)-1] == i:
+ f.write(f"{i}")
+ else:
+ f.write(f"{i}\n")
+
+ print(splitnl)
+
+
+build:
+ @just incrementBuildNumber
+
+ pdc -q -sdkpath ~/Documents/PlaydateSDK/ src CS-16
+
+run:
+ PlaydateSimulator CS-16.pdx
+
+release:
+ just build
+ rm CS-16.pdx.zip
+ zip -rq CS-16.pdx.zip CS-16.pdx
diff --git a/src/SystemAssets/card-pressed.png b/src/SystemAssets/card-pressed.png
index 4ba5d0a..8989c5e 100644
Binary files a/src/SystemAssets/card-pressed.png and b/src/SystemAssets/card-pressed.png differ
diff --git a/src/SystemAssets/card.png b/src/SystemAssets/card.png
index b2382b6..8c3d548 100644
Binary files a/src/SystemAssets/card.png and b/src/SystemAssets/card.png differ
diff --git a/src/SystemAssets/launchImage.png b/src/SystemAssets/launchImage.png
index 94e25af..ae050a2 100644
Binary files a/src/SystemAssets/launchImage.png and b/src/SystemAssets/launchImage.png differ
diff --git a/src/buttons.lua b/src/buttons.lua
index d2a51ac..2d1cdda 100644
--- a/src/buttons.lua
+++ b/src/buttons.lua
@@ -3,12 +3,13 @@
pattern = {}
instrument = {}
song = {}
+fx = {}
pattern.recording = false
instrument.selectedInst = 0
instrument.allMuted = false
-instrument.samplePreviewElems = {playdate.sound.track.new(), playdate.sound.sequence.new()}
+instrument.samplePreviewElems = { playdate.sound.track.new(), playdate.sound.sequence.new() }
instrument.sample = playdate.sound.sample.new(5)
instrument.copymode = "all"
@@ -19,6 +20,7 @@ instrument.movetrack = 0
function pattern.AButtonDown()
if pattern.recording == true then
toggleNote(math.quantize(currentSeqStep, settings["recordQuantization"]), settings["aRecTrack"])
+ updateInstsImage()
else
if seq:isPlaying() then
seq:stop()
@@ -35,19 +37,21 @@ end
function pattern.BButtonDown()
if pattern.recording == true then
toggleNote(math.quantize(currentSeqStep, settings["recordQuantization"]), settings["bRecTrack"])
+ updateInstsImage()
else
- crankMode = table.cycle(crankModes,crankMode)
+ crankMode = table.cycle(crankModes, crankMode)
local append = ""
if settings["num/max"] == true then
- append = " - "..table.find(crankModes,crankMode).."/"..#crankModes
+ append = " - " .. table.indexOfElement(crankModes, crankMode) .. "/" .. #crankModes
end
- displayInfo(crankMode..append)
+ displayInfo(crankMode .. append)
end
end
function pattern.upButtonDown()
if pattern.recording == true then
toggleNote(math.quantize(currentSeqStep, settings["recordQuantization"]), settings["upRecTrack"])
+ updateInstsImage()
else
if cursor[2] ~= 0 then
cursor[2] -= 1
@@ -58,8 +62,9 @@ end
function pattern.downButtonDown()
if pattern.recording == true then
toggleNote(math.quantize(currentSeqStep, settings["recordQuantization"]), settings["downRecTrack"])
+ updateInstsImage()
else
- if (cursor[2]+1)*16 < stepCount then
+ if (cursor[2] + 1) * 16 < (stepCount / 8) then
cursor[2] += 1
end
end
@@ -68,6 +73,7 @@ end
function pattern.rightButtonDown()
if pattern.recording == true then
toggleNote(math.quantize(currentSeqStep, settings["recordQuantization"]), settings["rightRecTrack"])
+ updateInstsImage()
else
if cursor[1] ~= 15 then
cursor[1] += 1
@@ -78,6 +84,7 @@ end
function pattern.leftButtonDown()
if pattern.recording == true then
toggleNote(math.quantize(currentSeqStep, settings["recordQuantization"]), settings["leftRecTrack"])
+ updateInstsImage()
else
if cursor[1] ~= 0 then
cursor[1] -= 1
@@ -88,22 +95,32 @@ end
function instrument.AButtonDown() -- sorry, "track" screen in manual is referred to as "instrument" in source code. silly me :P
local selRow = listview:getSelectedRow()
if listviewContents[1] ~= "*Ā*" then
+ pd.getSystemMenu():removeAllMenuItems()
+
instrument.selectedInst = instrumentTable[selRow]
- listview:set({"*Ā*"})
- knobs[1]:setClicks(table.find(waveNames,trackNames[selRow])-1)
+ listview:set({ "*Ā*" })
+
+ local clickIndex = table.indexOfElement(waveNames, trackNames[selRow])
+
+ if clickIndex == nil then
+ knobs[1]:setClicks(0)
+ else
+ knobs[1]:setClicks(clickIndex - 1)
+ end
local adsr = instrumentADSRtable[selRow]
- for i=2,5 do
- knobs[i]:setClicks((adsr[i-1]*10))
+ for i = 2, 5 do
+ knobs[i]:setClicks((adsr[i - 1] * 10))
end
- knobs[6]:setClicks(instrumentParamTable[selRow][1]*10)
- knobs[7]:setClicks(instrumentParamTable[selRow][2]*10)
+ knobs[6]:setClicks(instrumentParamTable[selRow][1] * 10)
+ knobs[7]:setClicks(instrumentParamTable[selRow][2] * 10)
knobs[8]:setClicks(instrumentTransposeTable[selRow])
- knobs[9]:setClicks(instrument.selectedInst:getVolume()*10)
+ knobs[9]:setClicks(instrument.selectedInst:getVolume() * 10)
elseif currentElem == 1 then
instrument.updateList()
+ applyMenuItems("track")
elseif currentElem == 7 then
instrumentLegatoTable[selRow] = not instrumentLegatoTable[selRow]
instrument.selectedInst:setLegato(instrumentLegatoTable[selRow])
@@ -112,8 +129,9 @@ function instrument.AButtonDown() -- sorry, "track" screen in manual is referred
if trackNames[listview:getSelectedRow()] == "smp" then
mode = "newsmp"
end
+
filePicker.open(function(data)
- local file,image = nil, nil
+ local file, image = nil, nil
if type(data) == "table" then
file = data[1]
image = data[2]
@@ -121,86 +139,89 @@ function instrument.AButtonDown() -- sorry, "track" screen in manual is referred
file = data
end
- local newSample,err
+ local newSample, err
if type(file) == "string" then
- newSample,err = snd.sample.new(file)
+ newSample, err = snd.sample.new(file)
elseif type(file) == "userdata" then
newSample = file
end
if err == nil then
local filename
if type(file) == "string" then
- filename = string.split(file,"/")[#string.split(file,"/")]
+ filename = string.split(file, "/")[#string.split(file, "/")]
else
- filename = listview:getSelectedRow()..".pda"
+ filename = listview:getSelectedRow() .. ".pda"
end
instrument.selectedInst:setWaveform(WAVE_SIN)
instrument.selectedInst:setWaveform(newSample)
trackNames[listview:getSelectedRow()] = "smp"
- newSample:save("temp/"..listview:getSelectedRow()..".pda")
+ newSample:save("temp/" .. listview:getSelectedRow() .. ".pda")
if settings["savewavs"] == true then
- newSample:save("temp/"..listview:getSelectedRow()..".wav")
+ newSample:save("temp/" .. listview:getSelectedRow() .. ".wav")
end
if image ~= nil then
- pd.datastore.writeImage(image, "temp/"..listview:getSelectedRow()..".pdi")
+ pd.datastore.writeImage(image, "temp/" .. listview:getSelectedRow() .. ".pdi")
end
- displayInfo("loaded "..filename)
+ displayInfo("loaded " .. filename)
elseif file == "none" then
print("ERROR INTENTIONAL! No sample selected.")
else
- print("ugh, error... "..tostring(err))
+ print("ugh, error... " .. tostring(err))
end
- end,mode)
+ end, mode)
elseif currentElem == 9 then
local trk = instrument.samplePreviewElems[1]
local sequ = instrument.samplePreviewElems[2]
sequ:allNotesOff()
+
+ trk:setNotes({})
+
local inst = snd.synth.new()
local len = 1
if trackNames[selRow] == "smp" then
- instrument.sample:load("temp/"..selRow..".pda")
+ instrument.sample:load("temp/" .. selRow .. ".pda")
inst:setWaveform(WAVE_SIN)
inst:setWaveform(instrument.sample)
len = 100
else
- inst:setWaveform(waveTable[table.find(waveNames,trackNames[selRow])])
+ inst:setWaveform(waveTable[table.indexOfElement(waveNames, trackNames[selRow])])
end
local adsr = instrumentADSRtable[selRow] -- once fix is pushed, remove and replace with synth:copy()
- inst:setADSR(adsr[1],adsr[2],adsr[3],adsr[4])
+ inst:setADSR(adsr[1], adsr[2], adsr[3], adsr[4])
inst:setLegato(instrumentLegatoTable[selRow])
- inst:setParameter(1,instrumentParamTable[selRow][1])
- inst:setParameter(2,instrumentParamTable[selRow][2])
+ inst:setParameter(1, instrumentParamTable[selRow][1])
+ inst:setParameter(2, instrumentParamTable[selRow][2])
inst:setVolume(instrument.selectedInst:getVolume())
- sequ:setLoops(1,1,1)
+ sequ:setLoops(1, 1, 1)
trk:setInstrument(inst)
- trk:addNote(1,60+instrumentTransposeTable[selRow],len)
+ trk:addNote(1, 60 + instrumentTransposeTable[selRow], len)
sequ:addTrack(trk)
sequ:play(function(s)
s:stop()
end)
- instrument.samplePreviewElems = {trk, sequ}
+ instrument.samplePreviewElems = { trk, sequ }
end
end
function instrument.BButtonDown()
- local cur,max = table.find(crankModes,crankMode),#crankModes
+ local cur, max = table.indexOfElement(crankModes, crankMode), #crankModes
if listviewContents[1] == "*Ā*" then
- crankMode = table.cycle(crankModes,crankMode)
+ crankMode = table.cycle(crankModes, crankMode)
else
crankMode = "screen"
- cur,max = 1,1
+ cur, max = 1, 1
end
local append = ""
if settings["num/max"] == true then
- append = " - "..cur.."/"..max
+ append = " - " .. cur .. "/" .. max
end
- displayInfo(crankMode..append)
+ displayInfo(crankMode .. append)
end
function instrument.upButtonDown()
@@ -236,7 +257,7 @@ end
function instrument.rightButtonDown()
local selRow = listview:getSelectedRow()
- if currentElem ~= #knobs+#buttons and listviewContents[1] == "*Ā*" then
+ if currentElem ~= #knobs + #buttons and listviewContents[1] == "*Ā*" then
currentElem += 1
elseif listviewContents[1] ~= "*Ā*" then
tracksMutedTable[selRow] = not tracksMutedTable[selRow]
@@ -257,7 +278,7 @@ function instrument.leftButtonDown()
instrument.allMuted = not instrument.allMuted
local newVal = instrument.allMuted
- for i=1, #tracks do
+ for i = 1, #tracks do
tracksMutedTable[i] = newVal
tracks[i]:setMuted(newVal)
end
@@ -279,16 +300,22 @@ function instrument.updateList()
if tracksMutedTable[i] == true then
append = " *ćč*"
else
- append = append.." *č*"
+ append = append .. " *č*"
end
end
- table.insert(finalListViewContents, tostring(i).." - "..trackNames[i]..append)
+
+ local name = trackNames[i]
+
+ if userTrackNames[i] ~= "" then
+ name = userTrackNames[i]
+ end
+
+ table.insert(finalListViewContents, tostring(i) .. " - " .. name .. append)
end
listview:set(finalListViewContents)
end
-
function song.AButtonDown()
if seq:isPlaying() then
seq:stop()
@@ -302,12 +329,12 @@ function song.AButtonDown()
end
function song.BButtonDown()
- crankMode = table.cycle(crankModes,crankMode)
+ crankMode = table.cycle(crankModes, crankMode)
local append = ""
if settings["num/max"] == true then
- append = " - "..table.find(crankModes,crankMode).."/"..#crankModes
+ append = " - " .. table.indexOfElement(crankModes, crankMode) .. "/" .. #crankModes
end
- displayInfo(crankMode..append)
+ displayInfo(crankMode .. append)
end
function song.upButtonDown()
@@ -325,3 +352,91 @@ end
function song.leftButtonDown()
end
+
+fx.enabled = false
+fx.selectedEffect = 0
+
+function fx.AButtonDown()
+ fx.enabled = not fx.enabled
+
+ if fx.enabled == false then
+ for i, v in ipairs(CS16effects) do
+ v:disable()
+ end
+ end
+end
+
+function fx.BButtonDown()
+ crankMode = table.cycle(crankModes, crankMode)
+ local append = ""
+ if settings["num/max"] == true then
+ append = " - " .. table.indexOfElement(crankModes, crankMode) .. "/" .. #crankModes
+ end
+ displayInfo(crankMode .. append)
+end
+
+function fx.upButtonDown()
+ if fx.enabled then
+ tapeEffect:enable()
+ else
+ fx.selectedEffect = 1
+ end
+
+ if tapeEffect:getLocked() and fx.enabled then
+ tapeEffect:setLocked(false)
+ end
+end
+
+function fx.upButtonUp()
+ tapeEffect:disable()
+end
+
+function fx.downButtonDown()
+ if fx.enabled then
+ waterEffect:enable()
+ else
+ fx.selectedEffect = 3
+ end
+
+ if waterEffect:getLocked() and fx.enabled then
+ waterEffect:setLocked(false)
+ end
+end
+
+function fx.downButtonUp()
+ waterEffect:disable()
+end
+
+function fx.rightButtonDown()
+ if fx.enabled then
+ bitcrushEffect:enable()
+ else
+ fx.selectedEffect = 2
+ end
+
+ if bitcrushEffect:getLocked() and fx.enabled then
+ bitcrushEffect:setLocked(false)
+ end
+end
+
+function fx.rightButtonUp()
+ bitcrushEffect:disable()
+end
+
+function fx.leftButtonDown()
+ if fx.enabled then
+ overdriveEffect:enable()
+ else
+ fx.selectedEffect = 4
+ end
+
+ if overdriveEffect:getLocked() and fx.enabled then
+ overdriveEffect:setLocked(false)
+ end
+end
+
+function fx.leftButtonUp()
+ overdriveEffect:disable()
+end
+
+-- burmger
diff --git a/src/consts.lua b/src/consts.lua
index 54e150e..80f3f77 100644
--- a/src/consts.lua
+++ b/src/consts.lua
@@ -21,22 +21,31 @@ WAVE_POD = snd.kWavePODigital
WAVE_POV = snd.kWavePOVosim
crankModesList = {
- {"note status","pitch","length","velocity","track","screen"},
- {"turn knob","screen"},
- {"tempo","pattern length","screen"}
+ { "note status", "pitch", "length", "velocity", "swing", "track", "screen" },
+ { "turn knob", "screen" },
+ { "lock effect", "effect intensity", "screen" },
+ { "tempo", "pattern length", "screen" }
}
+-- eggect
+
fnt = gfx.font.new("fnt/modified-tron")
+fnt8x8 = gfx.font.new("fnt/modified-tron-8x8")
+
+rains1x = gfx.font.new("fnt/font-rains-1x")
+rains2x = gfx.font.new("fnt/font-rains-2x")
+
+rains1x:setLeading(2)
+-- rains2x:setLeading(2)
+
gfx.setFont(gfx.getSystemFont(), gfx.font.kVariantItalic)
gfx.setFont(fnt, gfx.font.kVariantBold)
gfx.setFont(fnt)
-fnt8x8 = gfx.font.new("fnt/modified-tron-8x8")
-
MIDInotes = {}
for i = 1, 21, 1 do
- table.insert(MIDInotes,"")
+ table.insert(MIDInotes, "")
end
MIDInotes = {
diff --git a/src/draw.lua b/src/draw.lua
new file mode 100644
index 0000000..e129488
--- /dev/null
+++ b/src/draw.lua
@@ -0,0 +1,203 @@
+-- drawing functions
+
+function drawCursor()
+ gfx.setColor(gfx.kColorXOR)
+ gfx.setLineWidth(3)
+ gfx.drawRect((cursor[1] * 25) + 1, (cursor[2] * 25) + 1, 23, 23)
+ gfx.setLineWidth(1)
+ gfx.setColor(gfx.kColorBlack)
+end
+
+function drawNoteOn()
+ local modCurrentSeqStep = seq:getCurrentStep() / 8
+ local markerX, markerY
+
+ markerY = math.floor((modCurrentSeqStep - 1) / 16) * 25
+ markerX = ((modCurrentSeqStep - 1) % 16) * 25
+
+ local olddraw = gfx.getImageDrawMode()
+ gfx.setImageDrawMode(gfx.kDrawModeNXOR)
+
+ if seq:isPlaying() then
+ noteOn:draw(markerX, markerY)
+ end
+
+ gfx.setImageDrawMode(olddraw)
+end
+
+local lasty
+
+function drawSteps()
+ currentSeqStep = seq:getCurrentStep()
+
+ currentStepsImage:draw(0, 0)
+
+ local activeStr = ""
+ for i = 1, #tracks do
+ activeStr = activeStr .. tostring(tracks[i]:getNotesActive())
+ end
+
+ gfx.drawTextAligned(activeStr, 200, lasty + 27, align.center)
+end
+
+function updateStepsImage() -- optimization, babyyyy!
+ gfx.pushContext(currentStepsImage)
+
+ gfx.clear(gfx.kColorClear)
+
+ local markerX, markerY
+ lasty = 0
+
+ local olddraw = gfx.getImageDrawMode()
+ gfx.setImageDrawMode(gfx.kDrawModeWhiteTransparent)
+
+ for i = 1, stepCount / 8 do
+ markerY = math.floor((i - 1) / 16) * 25
+ markerX = (((i - 1) % 16) * 25)
+
+ noteOff:draw(markerX, markerY)
+ lasty = markerY
+ end
+
+ gfx.setImageDrawMode(olddraw)
+
+ gfx.popContext()
+end
+
+function updateInstsImage()
+ gfx.pushContext(currentInstsImage)
+
+ gfx.clear(gfx.kColorClear)
+
+ local notes = selectedTrack:getNotes()
+ for i = 1, #notes do -- replace with 1 if no swing
+ local step = (notes[i]["step"] / 8) - 1
+ if step >= stepCount / 8 then
+ break
+ end
+ local stepx, stepy
+ if step > 15 then
+ stepx = (step % 16) * 25
+ stepy = (math.floor(step / 16)) * 25
+ else
+ stepx = (step) * 25
+ stepy = 0
+ end
+
+ local olddraw = gfx.getImageDrawMode()
+ gfx.setImageDrawMode(gfx.kDrawModeWhiteTransparent)
+
+ notePlaced:fadedImage(notes[i]["velocity"], gfx.image.kDitherTypeBayer4x4):draw(stepx, stepy)
+
+ gfx.setImageDrawMode(gfx.kDrawModeFillWhite)
+
+ -- this goofy ahh solution is used instead of drawTextInRect.
+ -- mainly because...
+ -- a) drawTextInRect is VERY memory intensive
+ -- b) it's much faster
+
+ if settings["showNoteNames"] then
+ gfx.fillRect(stepx+4, stepy+4, 8, 17)
+
+ local text = MIDInotes[notes[i]["note"] - 20]
+ local tagoroctave = string.sub(text, 2, 2)
+
+ if tagoroctave == "#" then
+ fnt8x8:drawText(string.sub(text, 1, 2), stepx + 4, stepy + 4)
+ fnt8x8:drawText(string.sub(text, 3, 3), stepx + 4, stepy + 12)
+ else
+ fnt8x8:drawText(string.sub(text, 1, 1), stepx + 4, stepy + 4)
+ fnt8x8:drawText(tagoroctave, stepx + 4, stepy + 12)
+ end
+ --gfx.drawTextInRect(MIDInotes[notes[i]["note"]-20], stepx+4, stepy+4, 16, 20, nil, nil, nil, fnt8x8)
+ end
+
+ gfx.setImageDrawMode(olddraw)
+ end
+
+ gfx.popContext()
+end
+
+function drawInsts()
+ local oldDraw = gfx.getImageDrawMode()
+
+ gfx.setImageDrawMode(gfx.kDrawModeCopy)
+ currentInstsImage:draw(0, 0)
+ gfx.setImageDrawMode(oldDraw)
+end
+
+function drawFxTriangle(ptx, pty, direction, size, fill)
+ local point1x, point1y, point2x, point2y
+
+ if direction == nil then
+ direction = "n"
+ end
+
+ if direction == "n" then
+ point1x = ptx - size
+ point1y = pty + size
+ point2x = ptx + size
+ point2y = pty + size
+ elseif direction == "s" then
+ point1x = ptx - size
+ point1y = pty - size
+ point2x = ptx + size
+ point2y = pty - size
+ elseif direction == "e" then
+ point1x = ptx - size
+ point1y = pty - size
+ point2x = ptx - size
+ point2y = pty + size
+ elseif direction == "w" then
+ point1x = ptx + size
+ point1y = pty - size
+ point2x = ptx + size
+ point2y = pty + size
+ end
+
+ if fill then
+ gfx.fillTriangle(ptx, pty, point1x, point1y, point2x, point2y)
+ else
+ gfx.drawTriangle(ptx, pty, point1x, point1y, point2x, point2y)
+ end
+end
+
+function drawCenteredScaled(img, x, y, scale)
+ local imgSizeW, imgSizeH = img:getSize()
+
+ local modImg = gfx.image.new(imgSizeW * 2, imgSizeH * 2, gfx.kColorClear)
+ gfx.pushContext(modImg)
+ img:drawScaled(0, 0, scale)
+ gfx.popContext()
+ modImg:drawCentered(x, y)
+end
+
+
+-- visualizer particle class
+
+class("Particle").extends()
+
+function Particle:init()
+ if x ~= nil then
+ self.x = x
+ else
+ self.x = math.random(1, 400)
+ end
+ self.y = math.random(20, 220)
+ self.xrestart = 410
+ self.speed = math.random() + math.random(1, 1)
+ self.radius = self.speed
+end
+
+function Particle:update()
+ if self.x > self.xrestart then
+ self.x = -20
+ self.y = math.random(20, 220)
+ self.speed = math.random() + math.random(1, 1)
+ self.radius = self.speed
+ else
+ self.x += self.speed
+ end
+
+ gfx.fillCircleAtPoint(self.x, self.y, self.radius)
+end
diff --git a/src/fnt/font-rains-1x.fnt b/src/fnt/font-rains-1x.fnt
new file mode 100644
index 0000000..c1c7aac
--- /dev/null
+++ b/src/fnt/font-rains-1x.fnt
@@ -0,0 +1,102 @@
+--metrics={"baseline":0,"xHeight":0,"capHeight":0,"pairs":{},"left":[],"right":[]}
+datalen=2192
+data=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
+width=8
+height=8
+
+tracking=2
+
+0 7
+1 7
+2 7
+3 7
+4 7
+5 7
+6 7
+7 7
+8 7
+9 7
+space 7
+! 6
+" 7
+# 7
+$ 7
+% 6
+& 8
+' 5
+( 6
+) 6
+* 6
++ 7
+, 5
+- 7
+. 4
+/ 7
+: 3
+; 3
+< 5
+= 6
+> 6
+? 7
+@ 7
+A 7
+B 7
+C 7
+D 7
+E 7
+F 7
+G 7
+H 7
+I 6
+J 7
+K 7
+L 6
+M 7
+N 7
+O 7
+P 7
+Q 7
+R 7
+S 7
+T 6
+U 7
+V 7
+W 7
+X 7
+Y 6
+Z 7
+[ 5
+\ 7
+] 5
+_ 7
+` 5
+a 7
+b 7
+c 7
+d 7
+e 7
+f 7
+g 7
+h 7
+i 6
+j 7
+k 7
+l 6
+m 7
+n 7
+o 7
+p 7
+q 7
+r 7
+s 7
+t 6
+u 7
+v 7
+w 7
+x 7
+y 6
+z 7
+| 4
+~ 7
+� 7
+
diff --git a/src/fnt/font-rains-2x.fnt b/src/fnt/font-rains-2x.fnt
new file mode 100644
index 0000000..f4194bb
--- /dev/null
+++ b/src/fnt/font-rains-2x.fnt
@@ -0,0 +1,102 @@
+--metrics={"baseline":0,"xHeight":0,"capHeight":0,"pairs":{},"left":[],"right":[]}
+datalen=4844
+data=iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAYAAACLz2ctAAAN+ElEQVR4Xu2dWWIjOQ5Eu+5/6J5xy3LJFJEvAiCVWjC/SWyBwELKU/3nn/5fI3AiAn9OtN2mG4F/IgL+G2DjEHbUochGdkd3XL+jVJNPKg6q31c/snYjedd+Vk/V7zu7biLJgdtENwHjDkc4EqFGeTqvFiDpqfqNBLw6QAEeOaJ2jQxZM6T+suPKuYlw9UeEyOpR5aJzrnyWH3d2IkVZA7NkK2PHBUDRmSH4VaYJOC+RaoMKCbiDAKpOZxV3da46T3qIsLTDkn4Ho9lZ0q9OrSbggK67k2TPZxOY3b3IT5eQWf9XT8iX6YDUUShBrrybILLv7njkL3VQ15563rVrx3EFkhIQ7UbOZcRJGgVCulx5ip++qx1JvQSQPoo/k6+j/V3duQn3O0I/GwFXJVqt8Ch+IoCakFGPSkD1eax6biQqXT7djojxNwEvELmV2wS8IKB24rAjE+PVJXTWMZxu5pyt2MraITn6TiOxKo+dBlp6dLut6qW47xhMQKiOzrrKrn3RsUXxuaMbAR4UvtoIfjgBI0CVkeOOsVtCurKRP6qe8ugImFq1X5V3C4IIFhWkunvizugqcroYdP1f3VcFngJS9TQB59mp4kfyd7i7iSBS9fdGwEKgCWjB1YdXI/AIArqLf3VRX4WR67dzGSIfM7ZnOit6onFKnLHkSBkBpXx3QWgC+n8+lr29H+XPItKNIktuJwEtRw4CcB7LV5CXFumjFwEqNnrGIv/dfJE/StdUXkDSetyAlI4XPQWogVASMgRwEtEEnGfZ5YrUgFylOwm4kniqn7O3SKfjqnao8416nIJRuo+T56ptKxbHMRXsbAdsAv5FuEqCinxF1i6GRxJwp62I9JluRiPYiaPa+cZ1w9XnNo2VN3nKyX/fHTDdYFZXkmK/0kWpc6s77K2fLmFob3L1KZhZIzOh8JAHjyDgThsqaSLcnJ8Wx26qyKqxryicBDemIqsbRxPwIDMKiTJj0O1UTcBV5XOynkoiafdTRnBEPOoq1e8rYac1QLUl6VFHhGr07HNNwHoGJOIIZiQ9Owjojh8hllOOUFdynFJ1UdJUPY5v0VnyZYlcEzBO1cpkq7oo6aqejybgiuBbx4cgsKMDfgh0HeYKBJqAK1D0dbzSnrx17DcBffKskGgCfqNIBKwAtbVyVrDgBB0KnlXcqvJXWFbpOYS5CfhYFjYBB7xHAq74NeChFfRY/qStKcRbhVu1c7nyKmeu8f3iXBMwzSlLsAn4F65DAkZVOGXvkIJSJVjpnB8+2/7MK4d4CyAoqXA7X8kYEYpe5I/AVh2j/VPVQ0UT6VltvwnoZuz/5599BJ9SlQkcR5FK56vG/Gj50uRpAi5g20RFEzDGVdoBx7GWGVfVSvzyYYWOLMUytivEe5Zb8Co/JNyJWBVAMwmMRhntrFKw5qGM/xW8ViU+47ey0xNXTHgvx7coTXnyW2gViBVXHB9WEK/i685p4eBgx9AEjCFzgG8C2tR77g6YDKfFXg2BZ+2Ar4Zj+5tE4JaAzshRltbIJef/CpnRMduH6BJTesuaOEn6Vj9/kb3Rxax90hNdpMI8NgEv0FQTOAJM+rIEiIqX7BFxXPlVBf1nFpDbCdXzR+dW6Di6CZJ++h5VNhFJ/U72q99pQJJ+klfxudPTBJx3QLXTqASjBBEBqt+JQKSf5Cm+UN7Zx7JJUZyLAKDRcPVJBVC148aq2o8SocpX/a/aXy7fBFzTAVUCVRP4UQRUOtdRa466lzP2z+qANHLc0Uv6VAKrBAxvncEHwtm9xBA+P5cY5R1QBUe9CTYB71mgYvxRBFRBIeJVSK52UbeCx2cENVaVAErMt7i9iv3lK8SKHbAJWP+zsSbgDYtUMGivcb5Xbary1Q5GdrLfs3LVPX2VvKrnLk5nH3MI5Z6lBJA+Vb4JOEdSxa+ah0MCPsqJzM25egujCnV3SNrxSJ96S6S4o7iIKNkdWNVL8U9vwU1AFV7+Q15KQBPwgvX0t2A9DX2yESgiQKOkqL7FG4FjBFb9lvplhcaOus84O+JO/53iVNeXR1+CIiyzK8C4O476VQ782N+ZQCp+J8HuJcL9Y4JZATn+NQEvGTqVgFQNTkIj8mY7iEIQ5Yzq19gposREDSDqNNn4dxewil34DJNWcNDmVJ3UKW+/ZxOg+KKcaQL+RqA8QcsKnpyADqmcs9Tt1aKiDkiddFX+1NizDSDswKsCmAGuBqUm62hPqwKzegdUY2oCfiOlLo8jsDv+mOEoeSrRHD9pP1LIVMVvtbx7CVObhYq/bF+twCgJTUDv9hcVRhNwQEZm8JPugI8uGOoMtOqQPMWzuoNR16d3RLWx/fwUVw3g2XZASthqf4lATcB5Rp6GgNUCIAIoO6HqQ4W8qp/UYdTbMd3WqTCoE6r7c4it64CTpB1n3QRGu9VsxXD8VROrniPbFAfJE1FU+YiQJP8QArqLtEICdZS6AIwdZPYMQ9V/6z/ZX0UAsqPmgDps9lcrsj82vKUjmIxnxmAT8DcCb0tAqvb+3ghsQSDbarc400o/DwH1vUYZn8oe5TxcUzaeaQej0fiIHZB8OPrurk+zHTqK8ZA7TcALPJS83QSq2ldieCkCur+EEID0fdbtHBn1rPuMs+oZxSXw2GHoGaZKQJo2in2KcWpj1TsgEYC+NwHnFKAJ5awhmRzQWFWe0g7vGY8iIFXYuxLwmnQVZ3UXyyQ+Q0Cn8z11B2wCartmE/CbKfRSHt2CKgCq+1alW6o7oLuDqXEf3ghvPmY6D43LKGcKnhXZJSNYBURNhPL+6IwM9WwTkP9Vh9mF5uEEjEamQpyjcasSRbnV3dpR9dK5bAGpctQBVT1KHijW1Z1v6Q7YBDzeWtUVhXbfrJ63J6Bz/Xe6lVOVO86qOtWVg/YuFUeyR9+Vbpb5FUohemV3//nPtWZ3I7ftqgRwSO2cVe27Cafzu7+/LQHVCqdztPtUAdxh/5bY1AmyxFbfB91Cd4ry6NLhrhCKrl9YqgBQBe8ggJrUo8tIdZe9+tAEnCOpPIgfNh8CliqgvzcCJQSagCX4WriKwDiCVxCS3rJW2KC4M+NbGeXqXwmRf4SBiyGdv/qzy3/V/t04bgLOqaLuvO7uS0RQ9a1+P3QJlLUfEnAMnCqUKvyom6zUHfmR7YDqcxQlQO00rp5d/mXxytzOf+UsAmAlSarBOWSvArIrwapfhNUu/8gu5SAtT5W6gohp5yhq4btqu3ruUfLVjp8lMEGtxn+npwl4gUQFsJrAqvzHEHDlTqgml6ps53fVR5VA5Ku7+5G+R/lPDYv8vLuE0YhVfwk4MqyCozq/45zqYxNwjn76Fk0EXNEJ1eTuIJaq0wVwrGQ1RpXAal7Uyw2dU/2vrgDyDjgerHTCanAqiSrnmoC/0dtVAGkCVjrhKxJQ3XXUP+agDlT9ThhXv1Nxk/5QPst0Ry7tHEW98LvqY3WEkp3s96ycSnyCmuwvI6DSCWmc0Q2Qgr27SakCB+dUAFUCkktUwLswVDs7+Z/dgcsjuAk435WIMCPwTcBvRAgItRL6XCOQQqAJmIKthVYhUN0JVt4CV4+xGUbqDqficntO3SMdv8azO/3/sqXmoLrH/8irQEeEbwL+RaYJqLfFkIBZEFU59dysGlesC9kOovitnIlSFHWeqMDpFhq9FGTjHy+fZF9tbD//Sj4ZIG6r4KvnmoAXxD+GgA4xduwwFZ1UHLff3Q7g4OKcpd1OjYkIqnYq1XcXP4rT/u+EqCNEbsEHSKugqMk66qpVYKsdW13+x1g/hoDZHYXIcbTXnUnAKNFH8VT83U3AyO9Vl0g19rtzqgNNQCol/a+qlXWDrWk7IulR8093hO0E3ObAk45g6hgKiZxbO60ARBSSp3jSBPpW7Hbwu2eY1Q68+g5ICWsC1jpw+h1QrbRHE3BXAdHqodywaQw6F5gs/hTHLvwwdvpJRf1eDaDaUar21cSOzxpNwAsCKv53eVY7FY0k1YGjc+k94sY5VYdaWNHuOyOialuRVfNCu+GYN8qTGoOLX9gJ1UCbgHMEjv4YgcZP5g8Z1E6t5pWIRHmnyylh8PNTDx7sA43ADgScp4Id9lvnhyOwqwUTrOqIGPU4v5w4PtBuRKPm1eWVi8TK/f1nD24Cere4XTvY2QQ+nYBnA0D26fus2zky6tlXIeB406Z3wCoBadqE9ldd43cnUNV/C4Qjo55tAv5F2Lk/vDwBqcLetQNeE6c2iuw73q4OiJ1XDexZlnCHiGpXU8A/K/4m4JDx7AjCSvi2U6ngkZxnEtDdwdS41VcBFW9lcqg2j9afEY+fs4/qgCogaiKU/aMJeE+vDG6nEpAIERE4GpUKAEdjdgepVozgaESrOBDOq/UoeXCwpvhDe9QBCZgm4G9qEF7RKFLlqBupel6GgLR80/fs6FV/KXF+GVlxVu0Katxq5yB91e/KLujgF+58UQURsNnvBAwR+BEXi5UjWCUUxbXqEkMTyiGVc9YmIAEXAUIjnPSu+imwWsHkJ42+aEcbn1HoHCUu+wrhFrpTlLOzUZx3OLoEGAnXBDyGugl4jM/dP81xe5zGLo0RlZxqtfS5N0TAmet0c6qMB/X2RiPqDVP03iHNSHVG5/uySQS/3WPUs++dvTeIrgn4Bkl85RCagK+cvTfwvQn4Bkl85RCeZZfqHfCVWVTwvQlYAK9F6wg0AesYtoYCAk3AAngtWkegCVjHsDUUEGgCFsBr0ToCz0RAJ5pn8dvxuc9OEOhENi1ORaAJeCr8bbwJ2Bw4FYH/AVhjTPsOa87PAAAAAElFTkSuQmCC
+width=16
+height=16
+
+tracking=2
+
+0 15
+1 14
+2 15
+3 15
+4 15
+5 15
+6 15
+7 15
+8 15
+9 15
+space 14
+! 12
+" 14
+# 15
+$ 14
+% 13
+& 15
+' 11
+( 12
+) 12
+* 13
++ 14
+, 11
+- 14
+. 10
+/ 15
+: 8
+; 8
+< 12
+= 12
+> 12
+? 14
+@ 15
+A 15
+B 15
+C 15
+D 15
+E 15
+F 15
+G 15
+H 15
+I 14
+J 15
+K 15
+L 14
+M 15
+N 15
+O 15
+P 15
+Q 15
+R 15
+S 15
+T 14
+U 15
+V 15
+W 15
+X 15
+Y 14
+Z 15
+[ 12
+\ 15
+] 12
+_ 14
+` 11
+a 15
+b 15
+c 15
+d 15
+e 15
+f 15
+g 15
+h 15
+i 14
+j 15
+k 15
+l 14
+m 15
+n 15
+o 15
+p 15
+q 15
+r 15
+s 15
+t 14
+u 15
+v 15
+w 15
+x 15
+y 14
+z 15
+| 14
+~ 14
+� 14
+
diff --git a/src/fnt/modified-tron-8x8.fnt b/src/fnt/modified-tron-8x8.fnt
index c848434..1464b7b 100644
--- a/src/fnt/modified-tron-8x8.fnt
+++ b/src/fnt/modified-tron-8x8.fnt
@@ -1,6 +1,6 @@
--metrics={"baseline":6,"xHeight":2,"capHeight":3,"pairs":{},"left":[],"right":[]}
-datalen=1752
-data=iVBORw0KGgoAAAANSUhEUgAAAFAAAABICAYAAABhlHJbAAAE50lEQVR4Xu1cwXYbMQhs/v+j2+d9XhdjhhkQ642TzSmpBIIRMKM99OvP9bOEwNfd+i/wsq9Hyzcb1T7yb3131m8xrZxv7W+/+xhYfJu9DcCDZQFSAKzYe9/Vv308UayV+HcAER7Q/xSADGC7bm9aDbgKhtKW2aWxC30A3gVwB0FtoahifAv6lpFaqDmCqvHDETcNYDYzoyo88gL8jGPnRxeG8nlcgE8gqwIWQDSUsxZFCfrq8PPJDvyO/1H7yQqI5k7HPwJwyj8CMPMPR5Dacr76OnbKYP+4PVUgIgbdk67qqEmSYDp2Yj2UaRUAGbV7Yc1Ylc3bzB/TbYpOZF0lna8CaOfS7jiSAl1d19F5LEEG0Or6dr4CIHpmeVBXhnxEHBnrRxXoz2cjIpMtsk5UALSstUsPPwuZ9OisdwCkug2wVCc+uQIRSVgd1w7g7vwIe3YBbN2PCK9bywDatokIJTzAAYTabIIllRHCKpTF90J8agt/nD57V8ChthEOr0gaP/Aj9++qQFZhlfUNu+8EoJdATKYw3elndJZrJKOY/wtAU0CjAEaV6Utb1kkJyyIWtEJd/VqkAMBIhlVoaB8ZrQCInmcVoXwkgBkLV4T1Y66rADLZ0tFxZ1TgqQCyFpjQgbcEuxVYjS+6QMbClw4UJFppix/S7AaydWUIZzNyUsZ47cl05r7f5id9jECfn5D4jdhO2avMyKMBZJpX0n1GVTzpQARMJF+iQazKiLMqMGrL7DUlv7TUFt4DQHJEqbDsZWDtMxJBLdZuQadTyyOMfZ2oyJyMhaOZhHRXh4U7F4xklPVFhTyagZVKs7ePWvQ7AuhnniLDoIxRWAoBFbXPfosZkzGW8y3PKnwlPnUEvbQ4auGSFvrNm1USYRX6jnUmQ5CcqjJw5AeqFFUHKjIlq2Ymkzr+mU3WGLJM8bovo+kqAJnwVPVjxHhKxaBqYxeFzqsC+ogxo+kOzVsbRhJIxkRkwEYEkzHRhUYklRWYJ7Ft7zSAVV2ZJV6RGVMAZi+lMoCVBJBuVBNTZUSmM5mMqVS8HxEwv3ezMKvQSgshEokqhY0A9QJf4rt0YMbVwtq7K5BVWGWdkVSVJDKdCSs00oHoMc8OYDOmYh/NIPWdnUkjJHOYpvyVAKp6UtGUEoB7yXc+J6k3mLUoq9AIEHUEMTWgkshLx0Y68EgAMxZmI8Cza/bqQLKj8u/Z6+Sx9lMAjJLN5ExU7eyCwqfrJIBKix2lA5nOU1sYjRgr0vc9Wy6XDhS0XrblKBmDnkLZDS+msplXvhBVzpOech5MFszkeiWZbC+LqXvO6QAqmiyaY5URo87BDogSgIoOtIcjJnsasvc/FHCUPSz5SIIxG2V9DMCVz0lHktaR1WcZ+KUjpmSMKhPYBaBqgBVgDI6af2mFqk8h9tRRAeyy8NkA0hZW5sC1J0CgwnI/FUA7P5WX0tOe7AuIBUxpoU8FWM0t3HcB+P9/LGLdmAI4ocGuCnRvyU8FpBr31cJVxNz+UQBXhOxiHqeZj7LwRBbqjU6cdboPxjydANGrJXsLM9DZeifOEZtpAFGiGQAqOOq+EWBUJwqAFYmTAchUPovlYwFUL6OboGqn7lPjHdnHbv12iFqB3QRVO3XfCDCqk+73Oe9/JTnVVt2n5j6yL/qg2nG8mhyzZ+udmEds/gEXXfFnddHJOwAAAABJRU5ErkJggg==
+datalen=1712
+data=iVBORw0KGgoAAAANSUhEUgAAAFAAAABICAYAAABhlHJbAAAEyklEQVR4Xu2cbXPcMAiEm///o5u5m9gl3MIuCPvi1P2UVkJCK14eeTr5+HP/WVLg48v6b7DKNo6GHzaqPVrfrt0Zf/i0sr+1f/zsfWD+Pe2tA14sK5AiYMXer139u/cH+VrxfxMw0iNcf0pAJrAdtzetOlwVQ0nL7NLYhe6CdwXcRFBTCEWMT0GfMlIKNUtQ1f+wxE0LmNVMFIVHXoCvcWx/dGHRefYL8AfIooA5gIpylqLRAX10+PpkC35n/VH7yQhAdaezfiTg1PqRgNn6YQlSU85HX8dOKeyXm1MVAnXQ7dBVjppsEoxjJ8YhplUEZK3dgzXrqqzeZusxblM4kWWVtL8qoK1L28IIBbpc1+E8dkAm0Or4c39FwOiZ5UVdKfKocWRdH0Wg35+ViAxbZE5UBLRda0MPXwsZenTGOwJSbgu6VMc/OQKjJmE5ru3A1+JH2LMLYOO+RHhuLQto0wY1FLiBEyhKs4kuqZQQFqHMv5fGp6bw5fjsLIch2wibV5DGF3y0/FkRyCKsMv7U7icJ6BGIYQrjTl+js7MijGLr3wKaABoVEEWmD22Zk5IuG3VBC+rq1yJFANZkWIRCe2S0ImD0PKuA8pECZl24AtZ7XVcFZNjS4bh3ROBbBWQpMMGBjwN2I7DqH7pA1oVvDhQQrTTFF2l2A9m4UoSzGskwhkW4sn/k/1bT7Lj0MSL6/BTBL+p2ylylRh4tIGNeifsMVXzjwEgYhC+oEKsY8a4IRGmZvabkl5aawpsDEY4oEZa9DKw9aiIsxdg4eiqyFPX+ShzYEco6H0WYT/MMizpduOt3ZudTGtbgqAZWIu2qAkoCmZpHBcw4Sr3hyS6HUiiL8OgiUXpvflaeiqUURpPvfwMKqE3kJ3yvYxgS4VS1A6N1QkpROVDBlOidqRyssz6zyTJGxhRTA+H5MgGtA8xZxpFHjDOfThfQdxkvIOxCrktZG8ZZlc9HrISwJufF9JTB1o+o5OX/GGefexQBK/adC+p2YVXA7KUEA6zaxssbJBx1poAZuEf4wzjx6f/ZXZhFqI8U9YJZCndSFAn44l/WOW/uExQ4OwJZhFXGWZOqZljGmeHHEoQx0WOebcC6asXes2OV2yL2jHCKIdF/KSBKwEMF3EK+8zlJvcEsRVmEIkHUEqQ2GZUy9vOiLnekgFkXZiUg41DGeasChiXltwjIOO8SAiopdhQHMs5TBYxKjIXtbc7zLDcHCqyXTalgTFaDVpvI4jF2c/96mAoQ6WOCF5O9BZmzlfFbQFAOqt8AUR2rRJBaBzuXJUVgxoFhETXe+AOwpxbivqyIKwdHCKbYsTnLAoZPGbdztFHoAPNcHD8y+mzwvGTEFAeqmBCRPtNJuQBWc9kerXH1KTQVgd0UfbeANIVb6t9GN0j75qi8lL7Nyb6A2ABTUuiqAameDc67Bfz3G4sYc6YCrkLsVaMvRRQF0dQIvLJAzPc7hZlCZHxUwGgvdZPFs7zF3Jav5S48cYLfLPaLPqzzdASNXi2PtdgNd8c7fo7YTAvY+ZigRqw6b0QYdRFFwAriZAKuRtdlBVQvo3tA1U6dp/o7Mm8yArsHVO3UeSPCqIt0v8/59VcOp9qq89Szj8xDH1Q7C68ejtmz8Y7PIzafD03xZ0iqhq8AAAAASUVORK5CYII=
width=8
height=8
@@ -41,7 +41,7 @@ T 8
U 8
V 8
W 8
-X 8
+X 7
Y 7
Z 8
a 7
diff --git a/src/fnt/modified-tron.fnt b/src/fnt/modified-tron.fnt
index 1a6a540..e77b6f8 100644
--- a/src/fnt/modified-tron.fnt
+++ b/src/fnt/modified-tron.fnt
@@ -1,6 +1,6 @@
---metrics={"baseline":16,"xHeight":6,"capHeight":9,"pairs":{},"left":[],"right":[]}
-datalen=4868
-data=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
+--metrics={"baseline":16,"xHeight":7,"capHeight":10,"pairs":{},"left":[],"right":[]}
+datalen=4608
+data=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
width=16
height=16
@@ -70,7 +70,7 @@ w 14
x 14
y 14
z 14
-- 13
+- 16
. 6
# 15
^ 1
@@ -83,14 +83,14 @@ z 14
ă 13
Ą 14
ą 14
-Ć 11
+Ć 10
/ 16
, 6
? 10
ć 14
-Ĉ 15
-ĉ 15
-Ċ 15
+Ĉ 16
+ĉ 16
+Ċ 16
( 6
) 8
[ 5
@@ -101,9 +101,10 @@ z 14
ę 1
Ě 1
ě 1
-Č 11
+Č 12
= 11
č 15
_ 14
* 6
+> 11
diff --git a/src/funcs.lua b/src/funcs.lua
index 7686662..b5efd6c 100644
--- a/src/funcs.lua
+++ b/src/funcs.lua
@@ -1,94 +1,8 @@
-- helper functions!
-function drawCursor()
- gfx.setColor(gfx.kColorXOR)
- gfx.setLineWidth(3)
- gfx.drawRect(cursor[1]*25,cursor[2]*25,25,25)
- gfx.setLineWidth(1)
- gfx.setColor(gfx.kColorBlack)
-end
-
-function drawSteps()
- currentSeqStep = seq:getCurrentStep()
- local markerX, markerY
- local lasty = 0
-
- for i = 1, stepCount do
- markerY = math.floor((i - 1) / 16) * 25
- markerX = (((i - 1) % 16) * 25)
-
- noteOff:draw(markerX, markerY)
- lasty = markerY
- end
-
- markerY = math.floor((currentSeqStep - 1) / 16) * 25
- markerX = ((currentSeqStep - 1) % 16) * 25
-
- if seq:isPlaying() then
- noteOn:draw(markerX, markerY)
- end
-
- local activeStr = ""
- for i=1, #tracks do
- activeStr = activeStr..tostring(tracks[i]:getNotesActive())
- end
-
- gfx.drawTextAligned(activeStr,200,lasty+27,align.center)
-end
-
-function drawInsts()
- local notes = selectedTrack:getNotes()
- local seqSegment = math.ceil(seq:getCurrentStep()/32)
- for i = 1, #notes, 1 do
- local step = notes[i]["step"]-1
- if step >= stepCount then
- break
- end
- local stepx, stepy
- if step > 15 then
- stepx = (step%16)*25
- stepy = (math.floor(step/16))*25
- else
- stepx = (step)*25
- stepy = 0
- end
-
- notePlaced:fadedImage(notes[i]["velocity"],gfx.image.kDitherTypeBayer4x4):draw(stepx,stepy)
-
- -- this goofy ahh solution is used instead of drawTextInRect.
- -- mainly because...
- -- a) drawTextInRect is VERY memory intensive
- -- b) it's much faster
-
- if settings["showNoteNames"] then
- local text = MIDInotes[notes[i]["note"]-20]
- local tagoroctave = string.sub(text, 2, 2)
-
- if tagoroctave == "#" then
- fnt8x8:drawText(string.sub(text, 1, 2), stepx+4, stepy+4)
- fnt8x8:drawText(string.sub(text, 3, 3), stepx+4, stepy+12)
- else
- fnt8x8:drawText(string.sub(text, 1, 1), stepx+4, stepy+4)
- fnt8x8:drawText(tagoroctave, stepx+4, stepy+12)
- end
- --gfx.drawTextInRect(MIDInotes[notes[i]["note"]-20], stepx+4, stepy+4, 16, 20, nil, nil, nil, fnt8x8)
- end
- end
-end
-
-function table.find(t, value) -- finds value in the provided table and returns the index of it. if the item is not found, returns -1.
- -- blah, realized that there was already a function for this in the sdk (which is probably faster), so this is mainly just a shorthand func now
- local found = table.indexOfElement(t, value)
- if found == nil then
- return -1
- else
- return found
- end
-end
-
function table.cycle(t, currentVal, backwards) -- returns the next value in the table after the currentVal.
local val
- local find = table.find(t,currentVal)
+ local find = table.indexOfElement(t, currentVal)
if find == -1 then
return t[1]
@@ -98,33 +12,134 @@ function table.cycle(t, currentVal, backwards) -- returns the next value in the
if currentVal == t[1] then
val = t[#t]
else
- val = t[find-1]
+ val = t[find - 1]
end
else
if currentVal == t[#t] then
val = t[1]
else
- val = t[find+1]
+ val = t[find + 1]
end
end
return val
end
-function table.join(t,t2)
+function table.join(t, t2)
local t1 = table.deepcopy(t)
- for i=1,#t2 do
- t1[#t1+1] = t2[i]
+ for i = 1, #t2 do
+ t1[#t1 + 1] = t2[i]
end
return t1
end
+function math.round(num, numDecimalPlaces)
+ local mult = 10 ^ (numDecimalPlaces or 0)
+ return math.floor(num * mult + 0.5) / mult
+end
+
+function math.nearest(table, number)
+ local smallestSoFar, smallestIndex
+ for i, y in ipairs(table) do
+ if not smallestSoFar or (math.abs(number - y) <= smallestSoFar) then
+ smallestSoFar = math.abs(number - y)
+ smallestIndex = i
+ end
+ end
+ return smallestIndex, table[smallestIndex]
+end
+
+function math.normalize(n, b, t)
+ return math.max(b, math.min(t, n))
+end
+
+function math.quantize(step, quant)
+ step -= 8
+ local swing = tracksSwingTable[table.indexOfElement(tracks, selectedTrack)]
+
+if math.abs(swing) ~= swing then
+ swing = 16 + swing
+ end
+
+ if step % 16 == swing then
+ step -= swing
+ end
+
+ quant *= 8
+ if step % quant ~= 1 and quant ~= 1 then
+ local t = {}
+ for i = 1, stepCount, 8 do
+ if i % quant == 1 then
+ table.insert(t, i)
+ end
+ end
+ local index, value = math.nearest(t, step)
+ return value + 7
+ end
+ return step + 7
+end
+
+function string.normalize(str)
+ return string.gsub(string.gsub(str, "_", "__"), "%*", "%*%*")
+end
+
+function string.unnormalize(str)
+ return string.gsub(string.gsub(str, "__", "_"), "%*%*", "%*")
+end
+
+function string.split(inputstr, sep)
+ local t = {}
+ for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
+ table.insert(t, str)
+ end
+
+ return t
+end
+
function getStepFromCursor(cursor)
- local step = (cursor[1]+1)
+ local step = (cursor[1] + 1)
if cursor[2] >= 1 then
- step += (16*cursor[2])
+ step += (16 * cursor[2])
+ end
+ return step * 8
+end
+
+function applySwing(newSwing, trackToSwing, updateAnyway, dontUpdateTable)
+ local trackIndex = table.indexOfElement(tracks, trackToSwing)
+ local currentSwing = tracksSwingTable[trackIndex]
+ local swingEd = currentSwing
+
+ newSwing = math.normalize(newSwing, -5, 5)
+
+ if math.abs(currentSwing) ~= currentSwing then
+ swingEd = 16 + swingEd
+ end
+
+ if newSwing ~= currentSwing or updateAnyway then
+ local notes = trackToSwing:getNotes()
+
+ for i = 1, #notes do
+ if notes[i]["step"] % 16 == swingEd then
+ notes[i]["step"] -= currentSwing
+ end
+ end
+
+ trackToSwing:setNotes(notes)
+
+ local notes = trackToSwing:getNotes()
+
+ for i = 1, #notes do
+ if notes[i]["step"] % 16 == 0 then
+ notes[i]["step"] += newSwing
+ end
+ end
+
+ if not dontUpdateTable then
+ tracksSwingTable[trackIndex] = newSwing
+ end
+
+ trackToSwing:setNotes(notes)
end
- return step
end
function modifyNote(note, attrib, val, step)
@@ -135,18 +150,19 @@ function modifyNote(note, attrib, val, step)
if attrib == "pitch" then
attrib = "note"
- if (note["note"] == 21 and val == -1) or (note["note"] == #MIDInotes+20 and val == 1) then
+ if (note["note"] == 21 and val == -1) or (note["note"] == #MIDInotes + 20 and val == 1) then
goto continue
end
elseif attrib == "length" then
- if (note["length"] == 1 and val == -1) then
+ if (note["length"] == 8 and val == -1) then
goto continue
+ else
+ val *= 8 -- NOTE: possible hi-res length changes??
end
elseif attrib == "velocity" then
if (note["velocity"] == 0 and val == -1) or (note["velocity"] == 1 and val == 1) then
goto continue
end
-
end
if attrib == "velocity" then
@@ -159,7 +175,7 @@ function modifyNote(note, attrib, val, step)
for i = 1, #notes, 1 do
if notes[i]["step"] == step then
notes[i][attrib] += val
- notes[i][attrib] = math.round(notes[i][attrib],1)
+ notes[i][attrib] = math.round(notes[i][attrib], 1)
end
end
selectedTrack:setNotes(notes)
@@ -167,31 +183,6 @@ function modifyNote(note, attrib, val, step)
::continue::
end
-function math.nearest(table, number)
- local smallestSoFar, smallestIndex
- for i, y in ipairs(table) do
- if not smallestSoFar or (math.abs(number-y) <= smallestSoFar) then
- smallestSoFar = math.abs(number-y)
- smallestIndex = i
- end
- end
- return smallestIndex, table[smallestIndex]
-end
-
-function math.quantize(step, quant)
- if step % quant ~= 1 and quant ~= 1 then
- local t = {}
- for i = 1, stepCount do
- if i % quant == 1 then
- table.insert(t, i)
- end
- end
- local index, value = math.nearest(t, step)
- return value
- end
- return step
-end
-
function screenAnim(back)
if settings["screenAnimation"] then
if back == true then
@@ -203,12 +194,12 @@ function screenAnim(back)
end
function getTempoFromSPS(steps) -- returns tempo from steps per second
- return (steps/4)*30
+ return (steps / 4) * 30
end
function getSPSfromTempo(tempo)
local newTempo = 0.0
- newTempo = (tempo/30)*4
+ newTempo = (tempo / 30) * 4
if newTempo == 0.0 then
newTempo = 16
end
@@ -216,35 +207,19 @@ function getSPSfromTempo(tempo)
return newTempo
end
-function table.merge(t,t2)
- for i=1,#t2 do
- t[#t+1] = t2[i]
- end
- return t
-end
-
-function string.normalize(str)
- return string.gsub(string.gsub(str, "_", "__"),"%*","%*%*")
-end
-
-function string.unnormalize(str)
- return string.gsub(string.gsub(str,"__","_"),"%*%*","%*")
-end
-
function displayInfo(text, time)
local ms = 1000
if time ~= nil then
ms = time
end
- textTimer = pd.timer.new(ms,nil)
+ textTimer = pd.timer.new(ms, function()
+ if screenMode == "track" then
+ listview.needsDisplay = true
+ end
+ end)
textTimerText = text
end
-function math.round(num, numDecimalPlaces)
- local mult = 10^(numDecimalPlaces or 0)
- return math.floor(num * mult + 0.5) / mult
-end
-
function toggleNote(step, track)
local notes
local trackObj
@@ -262,184 +237,10 @@ function toggleNote(step, track)
end
end
- trackObj:addNote(step, 60, 1)
+ trackObj:addNote(step, 60, 8)
::continue::
end
-function buildSave(name)
- print("saving song to "..name)
- pd.file.mkdir("songs/"..name)
- -- format
- --
- -- 1 = tracks
- -- |- 1 = name
- -- |- 2 = notes
- -- |- 3 = adsr
- -- |- 4 = legato
- -- |- 5 = params
- -- |- 6 = transpose
- -- -- 7 = volume/pan
- -- 2 = other
- -- |- 1 = tempo
- -- |- 2 = steps
- -- -- 3 = fx
- -- 3 = metadata
- -- |- 1 = author
- -- -- 2 = time and date
-
- local tmp = {{{},{},{},{},{},{}},{},{}}
-
- for i, v in ipairs(tracks) do
- tmp[1][1][i] = trackNames[i]
- tmp[1][2][i] = v:getNotes()
- tmp[1][3][i] = instrumentADSRtable[i]
- tmp[1][4][i] = instrumentLegatoTable[i]
- tmp[1][5][i] = instrumentParamTable[i]
- tmp[1][6][i] = instrumentTransposeTable[i]
- end
-
- for i,v in ipairs(pd.file.listFiles("temp/")) do
- if string.sub(v,#v-2) == "pda" or string.sub(v,#v-2) == "wav" then
- local smp,err = snd.sample.new("temp/"..v)
- if err ~= nil then
- print(err)
- end
- smp:save("songs/"..name.."/"..v)
- elseif string.sub(v, #v-2) == "pdi" then
- pd.datastore.writeImage(pd.datastore.readImage("temp/"..v), "songs/"..name.."/"..v)
- end
- end
-
- tmp[2][1] = seq:getTempo()
- tmp[2][2] = stepCount
-
- tmp[3][1] = settings["author"]
- tmp[3][2] = pd.getTime()
-
- pd.datastore.write(tmp,"songs/"..name.."/song",false)
- print("success!")
- return tmp
-end
-
-function loadSave(name)
- seq:stop()
- seq:allNotesOff()
- seq:goToStep(1)
-
- pd.file.delete("temp/",true)
- pd.file.mkdir("temp/")
-
- print("loading song from "..name)
- local tmp = pd.datastore.read(name.."song")
-
- for i, v in ipairs(tracks) do
- trackNames[i] = tmp[1][1][i]
- v:setNotes(tmp[1][2][i])
- instrumentADSRtable[i] = tmp[1][3][i]
- local adsr = instrumentADSRtable[i]
- instrumentTable[i]:setADSR(adsr[1],adsr[2],adsr[3],adsr[4])
-
- if trackNames[i] ~= "smp" then
- instrumentTable[i]:setWaveform(waveTable[table.find(waveNames,trackNames[i])])
- else
- local smp = snd.sample.new(name..i..".pda")
- instrumentTable[i]:setWaveform(WAVE_SIN)
- instrumentTable[i]:setWaveform(smp)
- smp:save("temp/"..i..".pda")
-
- local sampleImage = pd.datastore.readImage(name..i..".pdi")
- if sampleImage ~= nil then
- pd.datastore.writeImage(sampleImage, "temp/"..i..".pdi")
- end
-
- if settings["savewavs"] == true then
- smp:save("temp/"..i..".wav")
- end
- end
- instrumentLegatoTable[i] = tmp[1][4][i]
- instrumentTable[i]:setLegato(instrumentLegatoTable[i])
- instrumentParamTable[i] = tmp[1][5][i]
- instrumentTable[i]:setParameter(1, instrumentParamTable[i][1])
- instrumentTable[i]:setParameter(2, instrumentParamTable[i][2])
- instrumentTransposeTable[i] = tmp[1][6][i]
- tracks[i]:getInstrument():setTranspose(instrumentTransposeTable[i])
- if tmp[1][7] ~= nil then
- instrumentTable[i]:setVolume(tmp[1][7][i])
- end
- tracksMutedTable[i] = false
- tracks[i]:setMuted(false)
- instrument.allMuted = false
-
- seq:setTempo(tmp[2][1]) -- TODO: be sure to implement real world bpm changes once fix is pushed!
- sinetimer.duration = 400-getTempoFromSPS(seq:getTempo())
- stepCount = tmp[2][2]
- seq:setLoops(1,stepCount)
-
- songAuthor = tmp[3][1]
-
- for i=1,16 do
- instrumentTable[i]:stop()
- end
-
- local finalListViewContents = {}
-
- for i = 1, #trackNames, 1 do
- table.insert(finalListViewContents, tostring(i).." - "..trackNames[i])
- end
-
- listview:set(finalListViewContents)
- end
- cursor = {0,0}
- print("success!")
-end
-
-function saveSettings()
- pd.datastore.write(settings,"settings")
-end
-
-function loadSettings()
- local data = pd.datastore.read("settings")
- if data ~= nil then
- for k,v in pairs(settings) do
- if data[k] == nil then
- data[k] = v
- elseif type(data[k]) ~= type(v) then
- data[k] = v
- end
- end
-
- if data["pmode"] == true then
- pd.display.setRefreshRate(50)
- else
- pd.display.setRefreshRate(30)
- end
-
- if data["useSystemFont"] == true then
- fnt = gfx.getSystemFont()
- else
- fnt = gfx.font.new("fnt/modified-tron")
- end
-
- gfx.setFont(fnt)
-
- return data
- end
- return settings
-end
-
-function string.split(inputstr,sep)
- local t = {}
- for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
- table.insert(t, str)
- end
-
- return t
-end
-
-function math.normalize(n, b, t)
- return math.max(b, math.min(t, n))
-end
-
function applyMenuItems(mode)
pdmenu:removeAllMenuItems()
if mode == "song" then
@@ -447,36 +248,38 @@ function applyMenuItems(mode)
pdmenu:removeAllMenuItems()
local startname = "newsong"
if songdir ~= "temp/" then
- startname = string.split(songdir,"/")[#string.split(songdir,"/")-1]
+ startname = string.split(songdir, "/")[#string.split(songdir, "/") - 1]
end
- keyboardScreen.open("song name:",startname,20,function(name)
+ keyboardScreen.open("song name:", startname, 20, function(name)
if name ~= "_EXITED_KEYBOĀRD" then -- this is such a lame solution, but it works!
buildSave(string.unnormalize(name))
- displayInfo("saved song "..name)
- songdir = "/songs/"..string.unnormalize(name).."/ (song)"
+ displayInfo("saved song " .. name)
+ songdir = "/songs/" .. string.unnormalize(name) .. "/ (song)"
end
applyMenuItems("song")
end)
end)
local loadMenuItem, error = pdmenu:addMenuItem("load", function()
pdmenu:removeAllMenuItems()
- filePicker.open(function (name)
+ filePicker.open(function(name)
if name ~= "none" then
name = string.unnormalize(name)
- pd.file.delete("temp/",true)
+ pd.file.delete("temp/", true)
pd.file.mkdir("temp/")
- loadSave(string.sub(name,1,#name-7))
+ loadSave(string.sub(name, 1, #name - 7))
screenMode = "pattern"
crankModes = crankModesList[1]
pd.inputHandlers.pop()
pd.inputHandlers.push(pattern, true)
- local autoNoteMenuItem, error = pdmenu:addOptionsMenuItem("autonote", {"none","1","2","4","8","16","32"}, autonote, function(arg)
- autonote = arg
- end)
+ local autoNoteMenuItem, error = pdmenu:addOptionsMenuItem("autonote",
+ { "none", "1", "2", "4", "8", "16", "32" },
+ autonote, function(arg)
+ autonote = arg
+ end)
songdir = name
- displayInfo("loaded song "..string.normalize(string.split(name,"/")[#string.split(name,"/")-1]))
+ displayInfo("loaded song " .. string.normalize(string.split(name, "/")[#string.split(name, "/") - 1]))
if settings["playonload"] == true then
seq:play()
@@ -487,16 +290,17 @@ function applyMenuItems(mode)
print("no song picked")
applyMenuItems("song")
end
- end,"song")
+ end, "song")
end)
- local settingsMenuItem, error = pdmenu:addMenuItem("settings", function ()
+ local settingsMenuItem, error = pdmenu:addMenuItem("settings", function()
pdmenu:removeAllMenuItems()
settingsScreen.open()
end)
elseif mode == "pattern" then
- local autoNoteMenuItem, error = pdmenu:addOptionsMenuItem("autonote", {"none","1","2","4","8","16","32"}, autonote, function(arg)
- autonote = arg
- end)
+ local autoNoteMenuItem, error = pdmenu:addOptionsMenuItem("autonote", { "none", "1", "2", "4", "8", "16", "32" },
+ autonote, function(arg)
+ autonote = arg
+ end)
local recordMenuItem, error = pdmenu:addMenuItem("record")
recordMenuItem:setCallback(function()
if recordMenuItem:getTitle() == "record" then
@@ -506,24 +310,39 @@ function applyMenuItems(mode)
sinetimer:reset()
sinetimer:start()
end
+
+ for index, track in ipairs(tracks) do -- NOTE: hacky solution inbound!!
+ applySwing(0, track, true, true)
+ end
+
+ updateInstsImage()
+
recordMenuItem:setTitle("stop record")
metronomeTrack:setMuted(false)
else
pattern.recording = false
recordMenuItem:setTitle("record")
metronomeTrack:setMuted(true)
+
+ for index, track in ipairs(tracks) do
+ applySwing(tracksSwingTable[index], track, true)
+ end
+
+ updateInstsImage()
end
end)
elseif mode == "track" then
local copyMenuItem, error = pdmenu:addMenuItem("copy")
- local copyModeMenuItem, error = pdmenu:addOptionsMenuItem("cpy mode", {"all", "inst", "ptn"}, "all", function(arg)
+ local copyModeMenuItem, error = pdmenu:addOptionsMenuItem("cpy mode", { "all", "inst", "ptn" }, "all", function(arg)
instrument.copymode = arg
end)
+ local renameMenuItem, error = pdmenu:addMenuItem("rename")
+
copyMenuItem:setCallback(function()
if instrument.copytrack ~= 0 then
local doInst = false
local doPattern = false
-
+
if instrument.copymode == "all" then
doInst = true
doPattern = true
@@ -534,65 +353,259 @@ function applyMenuItems(mode)
end
local row = listview:getSelectedRow()
- local track = instrument.copytrack
+ local track = instrument.copytrack -- copy from me!
instrument.copytrack = 0
- if doPattern then
- tracks[row]:setNotes(tracks[track]:getNotes())
- if seq:isPlaying() then
- seq:stop()
- seq:play()
- sinetimer:reset()
- end
- end
-
- if doInst then
- seq:stop()
- seq:allNotesOff()
-
- trackNames[row] = trackNames[track]
- if trackNames[row] == "smp" then
- local smp = snd.sample.new("temp/"..track..".pda")
- smp:save("temp/"..row..".pda")
- smp = snd.sample.new("temp/"..row..".pda")
- instrumentTable[row]:setWaveform(WAVE_SIN)
- instrumentTable[row]:setWaveform(smp)
- else
- local wv = waveTable[table.indexOfElement(waveNames, trackNames[row])]
- instrumentTable[row] = instrumentTable[track]:copy()
- instrumentTable[row]:setWaveform(wv)
- end
+ if row ~= track then
+ if doPattern then
+ tracks[row]:setNotes(tracks[track]:getNotes())
- tracks[row]:setInstrument(instrumentTable[row])
+ tracksSwingTable[row] = tracksSwingTable[track]
- instrumentADSRtable[row] = table.deepcopy(instrumentADSRtable[track])
- instrumentParamTable[row] = table.deepcopy(instrumentParamTable[track])
- instrumentLegatoTable[row] = instrumentLegatoTable[track]
- instrumentTransposeTable[row] = instrumentTransposeTable[track]
+ if seq:isPlaying() then
+ seq:stop()
+ seq:play()
+ sinetimer:reset()
+ end
+ end
- local adsr = instrumentADSRtable[row]
+ if doInst then
+ seq:stop()
+ seq:allNotesOff()
+
+ trackNames[row] = trackNames[track]
+ if trackNames[row] == "smp" then
+ local smp = snd.sample.new("temp/" .. track .. ".pda")
+ smp:save("temp/" .. row .. ".pda")
+ smp = snd.sample.new("temp/" .. row .. ".pda")
+ instrumentTable[row]:setWaveform(WAVE_SIN)
+ instrumentTable[row]:setWaveform(smp)
+ else
+ local wv = waveTable[table.indexOfElement(waveNames, trackNames[row])]
+ instrumentTable[row] = instrumentTable[track]:copy()
+ instrumentTable[row]:setWaveform(wv)
+ tracks[row]:setInstrument(instrumentTable[row])
+ end
+
+ instrumentADSRtable[row] = table.deepcopy(instrumentADSRtable[track])
+ instrumentParamTable[row] = table.deepcopy(instrumentParamTable[track])
+ instrumentLegatoTable[row] = instrumentLegatoTable[track]
+ instrumentTransposeTable[row] = instrumentTransposeTable[track]
+
+ local adsr = instrumentADSRtable[row]
+
+ instrumentTable[row]:setADSR(adsr[1], adsr[2], adsr[3], adsr[4])
+ instrumentTable[row]:setLegato(instrumentLegatoTable[row])
+ instrumentTable[row]:setParameter(1, instrumentParamTable[track][1])
+ instrumentTable[row]:setParameter(2, instrumentParamTable[track][2])
+ tracks[row]:getInstrument():setTranspose(instrumentTransposeTable[row])
+ instrumentTable[row]:setVolume(instrumentTable[track]:getVolume())
- instrumentTable[row]:setADSR(adsr[1],adsr[2],adsr[3],adsr[4])
- instrumentTable[row]:setLegato(instrumentLegatoTable[row])
- instrumentTable[row]:setParameter(1, instrumentParamTable[track][1])
- instrumentTable[row]:setParameter(2, instrumentParamTable[track][2])
- tracks[row]:getInstrument():setTranspose(instrumentTransposeTable[row])
- instrumentTable[row]:setVolume(instrumentTable[track]:getVolume())
+ seq:play()
+ end
- seq:play()
+ if instrument.copymode == "all" then
+ userTrackNames[row] = userTrackNames[track]
+ end
end
instrument.updateList()
copyMenuItem:setTitle("copy")
- displayInfo("pasted to track "..row)
+ displayInfo("pasted to track " .. row)
+
+ updateInstsImage()
else
instrument.copytrack = listview:getSelectedRow()
instrument.updateList()
copyMenuItem:setTitle("paste")
- displayInfo("copied track "..instrument.copytrack)
+ displayInfo("copied track " .. instrument.copytrack)
+ end
+ end)
+
+ renameMenuItem:setCallback(function()
+ pd.getSystemMenu():removeAllMenuItems()
+
+ local currentRow = listview:getSelectedRow()
+ local name = trackNames[currentRow]
+
+ if userTrackNames[currentRow] ~= "" then
+ name = userTrackNames[currentRow]
+ end
+
+ keyboardScreen.open("rename track", name, 10, function(newname)
+ if name == trackNames[currentRow] then
+ name = "default"
+ end
+
+ if newname ~= "_EXITED_KEYBOĀRD" then
+ userTrackNames[currentRow] = newname
+
+ if newname == "" then
+ displayInfo(name .. " >> default")
+ else
+ displayInfo(name .. " >> " .. newname)
+ end
+
+ instrument.updateList()
+ end
+
+ applyMenuItems(screenMode)
+ end)
+ end)
+ elseif mode == "songpicker" then
+ local log = logScreen
+
+ local deleteMenuItem, error = pdmenu:addMenuItem("delete")
+ local cloneMenuItem, error = pdmenu:addMenuItem("clone")
+ local renameMenuItem, error = pdmenu:addMenuItem("rename")
+
+ deleteMenuItem:setCallback(function()
+ local songName = filePickListContents[filePickList:getSelectedRow()]
+
+ if songName ~= "*Ā*" and string.sub(songName, #songName - 5) == "(song)" then
+ songName = string.sub(songName, 1, #songName - 8)
+
+ messageBox.open(
+ "are you sure you want to delete this song?\n\n\n\n" .. songName .. "\n\n\n\na = yes, b = no",
+ function(ans)
+ log.init()
+ if ans == "yes" then
+ songName = string.unnormalize(songName)
+
+ log.append("cs-16 v" .. pd.metadata.version .. " song eradicator")
+ log.append("")
+ log.append("attempting to delete song " .. songName)
+
+ for i, v in ipairs(pd.file.listFiles("songs/" .. songName)) do
+ pd.file.delete("songs/" .. songName .. "/" .. v)
+ log.append("deleted " .. v)
+ end
+
+ pd.file.delete("songs/" .. songName)
+
+ log.append("!! song successfully eradicated !!")
+
+ displayInfo("song " .. songName .. "deleted")
+
+ filePicker.updateFiles()
+ end
+
+ filePicker.update(true)
+ end)
+ end
+ end)
+
+ cloneMenuItem:setCallback(function()
+ local songName = filePickListContents[filePickList:getSelectedRow()]
+ local update = true
+
+ if songName ~= "*Ā*" and string.sub(songName, #songName - 5) == "(song)" then
+ songName = string.sub(songName, 1, #songName - 8)
+
+ keyboardScreen.open("what should the clone be called?", string.unnormalize(songName), 20, function(name)
+ if name ~= "_EXITED_KEYBOĀRD" and name ~= "" then
+ if name == songName then
+ local tempNewName = string.unnormalize(songName) .. "_clone"
+ name = string.sub(tempNewName, 1, 20)
+ end
+
+ if pd.file.exists("songs/" .. name) then
+ update = false
+ messageBox.open("song already exists!\n\na = ok", function()
+ filePicker.update(true)
+ end)
+ else
+ songName = string.unnormalize(songName)
+
+ log.init()
+
+ log.append("cs-16 v" .. pd.metadata.version .. " song cloner")
+ log.append("")
+ log.append("attempting to clone song " .. songName .. "...")
+
+ pd.file.mkdir("songs/" .. name)
+ log.append("created song directory")
+
+ log.append("cloning song data...")
+
+ for i, v in ipairs(pd.file.listFiles("songs/" .. songName)) do
+ log.append("attempting to clone " .. v .. "...")
+ if string.sub(v, #v - 3) == ".pda" then
+ log.append("file is a sample, using more efficient method...")
+ local sample = snd.sample.new("songs/" .. songName .. "/" .. v)
+ sample:save("songs/" .. name .. "/" .. v)
+ elseif string.sub(v, #v - 4) == ".json" then
+ log.append("found song json data, cloning...")
+
+ pd.datastore.write(pd.datastore.read("songs/" .. songName .. "/song"), "songs/" .. name .. "/song", false)
+ else
+ local oldfile = pd.file.open("songs/" .. songName .. "/" .. v)
+ local newfile = pd.file.open("songs/" .. name .. "/" .. v, pd.file.kFileWrite)
+
+ local data = oldfile:read(10000000) -- performance issue maybe could be??
+
+ newfile:write(data)
+
+ oldfile:close()
+ newfile:close()
+ end
+
+ log.append("cloned " .. v .. " successfully")
+ end
+
+ log.append("successfully cloned " .. songName)
+
+ filePicker.updateFiles()
+
+ local index = table.indexOfElement(filePickListContents, string.normalize(name) .. "/ (song)")
+
+ filePickList:setSelectedRow(index)
+ filePickList:scrollToRow(index)
+ end
+ end
+
+ if update then
+ filePicker.update(true)
+ end
+ end)
+ end
+ end)
+
+ renameMenuItem:setCallback(function()
+ local songName = filePickListContents[filePickList:getSelectedRow()]
+ local update = true
+
+ if songName ~= "*Ā*" and string.sub(songName, #songName - 5) == "(song)" then
+ songName = string.sub(songName, 1, #songName - 8)
+
+ keyboardScreen.open("what should the song name be?", string.unnormalize(songName), 20, function(name)
+ if name ~= "_EXITED_KEYBOĀRD" and name ~= "" then
+ if pd.file.exists("songs/" .. string.unnormalize(name)) then
+ update = false
+ messageBox.open("song already exists!\n\na = ok", function()
+ filePicker.update(true)
+ end)
+ else
+ pd.file.rename("songs/" .. string.unnormalize(songName), "songs/" .. string.unnormalize(name))
+
+ filePicker.update(true)
+
+ filePicker.updateFiles()
+
+ local index = table.indexOfElement(filePickListContents, string.normalize(name) .. "/ (song)")
+
+ filePickList:setSelectedRow(index)
+ filePickList:scrollToRow(index)
+ end
+ end
+
+ if update then
+ filePicker.update(true)
+ end
+ end)
end
end)
end
diff --git a/src/fx.lua b/src/fx.lua
new file mode 100644
index 0000000..82a2ac4
--- /dev/null
+++ b/src/fx.lua
@@ -0,0 +1,233 @@
+-- cs-16 effects!!
+
+class("CS16effect").extends()
+
+validEffects = {
+ ["tape"] = "",
+ ["wtr"] = "",
+ ["echo"] = "",
+ ["ovd"] = ""
+}
+
+validEffectsNames = {}
+
+for k, v in pairs(validEffects) do
+ table.insert(validEffectsNames, k)
+end
+
+appliedFX = {}
+
+function CS16effect:init(name, effects, intensities, params, notches)
+ self.name = name
+ self.effects = effects
+ self.intesities = intensities
+ self.params = params
+
+ if notches == nil then
+ self.notches = 10
+ else
+ self.notches = notches
+ end
+
+ self.overallValue = 0.0
+
+ self.isEnabled = false
+ self.locked = false
+
+ self.paramsValues = {}
+
+ local initVal
+
+ for i, v in ipairs(self.params) do
+ if type(intensities[i]) == "table" then
+ initVal = intensities[i][1]
+ else
+ initVal = intensities[i]
+ end
+ self.paramsValues[i] = initVal
+ end
+end
+
+function CS16effect:toggle()
+ self.isEnabled = not self.isEnabled
+ if self.isEnabled then
+ for i, v in ipairs(self.effects) do
+ playdate.sound.addEffect(v)
+ end
+ else
+ for i, v in ipairs(self.effects) do
+ playdate.sound.removeEffect(v)
+ end
+ end
+end
+
+function CS16effect:enable()
+ if not self.locked then
+ self:disable()
+
+ for i, v in ipairs(self.effects) do
+ playdate.sound.addEffect(v)
+ end
+
+ self.isEnabled = true
+ end
+end
+
+function CS16effect:disable()
+ if not self.locked then
+ for i, v in ipairs(self.effects) do
+ playdate.sound.removeEffect(v)
+ end
+
+ self.isEnabled = false
+ end
+end
+
+function CS16effect:getEnabled()
+ return self.isEnabled
+end
+
+function CS16effect:notch(num)
+ if num == nil then
+ num = 1
+ end
+
+ for i, effect in ipairs(self.effects) do
+ local intensity = self.intesities[i]
+ local lowIntensity = 0
+ if type(intensity) == "table" then
+ lowIntensity = intensity[1]
+ intensity = intensity[2]
+ end
+
+ if lowIntensity > intensity then
+ lowIntensity, intensity = intensity, lowIntensity
+ end
+
+ self:modifyParam(effect, self.params[i], ((intensity - lowIntensity) / self.notches) * num, i)
+ end
+
+ self.overallValue = math.round(math.normalize(self.overallValue + (0.1 * num), 0, 1), 2)
+end
+
+function CS16effect:modifyParam(effect, param, value, index)
+ if index == nil then
+ index = table.indexOfElement(self.effects, effect)
+ end
+
+ local curVal = self.paramsValues[index]
+
+ local lowIntensity = 0
+ local intensity = self.intesities[index]
+
+ if type(intensity) == "table" then
+ lowIntensity = self.intesities[index][1]
+ intensity = self.intesities[index][2]
+ end
+
+ if lowIntensity > intensity then
+ lowIntensity, intensity = intensity, lowIntensity
+ value *= -1
+ end
+
+ self.paramsValues[index] = math.round(math.normalize(value + curVal, lowIntensity, intensity), 2)
+
+ curVal = self.paramsValues[index]
+
+ -- parmasean
+
+ if param == "mix" then
+ effect:setMix(curVal)
+ elseif param == "gain" then
+ effect:setGain(curVal)
+ elseif param == "limit" then
+ effect:setLimit(curVal)
+ elseif param == "amount" then
+ effect:setAmount(curVal)
+ elseif param == "undersampling" then
+ effect:setUndersampling(curVal)
+ elseif param == "frequency" then
+ effect:setFrequency(curVal)
+ end
+end
+
+function CS16effect:getOverallValue()
+ return self.overallValue
+end
+
+function CS16effect:setName(name)
+ self.name = name
+end
+
+function CS16effect:getName()
+ return self.name
+end
+
+function CS16effect:setLocked(locked)
+ if locked == nil then
+ locked = true
+ end
+
+ self.locked = locked
+end
+
+function CS16effect:getLocked()
+ return self.locked
+end
+
+local tapeHiPass = playdate.sound.twopolefilter.new("hipass")
+tapeHiPass:setFrequency(700)
+tapeHiPass:setMix(0)
+
+local tapeLoPass = playdate.sound.twopolefilter.new("lopass")
+tapeLoPass:setFrequency(1300)
+tapeLoPass:setMix(0)
+
+local tapeOverdrive = playdate.sound.overdrive.new()
+tapeOverdrive:setGain(1)
+tapeOverdrive:setLimit(2)
+tapeOverdrive:setMix(0.3)
+
+
+local bitcrushBitcrush = playdate.sound.bitcrusher.new()
+bitcrushBitcrush:setAmount(0.6)
+bitcrushBitcrush:setUndersampling(0.4)
+bitcrushBitcrush:setMix(1)
+
+
+local waterLoPass = playdate.sound.twopolefilter.new("lopass")
+waterLoPass:setFrequency(1300)
+waterLoPass:setMix(0)
+
+
+local overdriveOverdrive = playdate.sound.overdrive.new()
+overdriveOverdrive:setLimit(2)
+overdriveOverdrive:setGain(2)
+overdriveOverdrive:setMix(0)
+
+-- if you can ever figure out how to get it to stay on beat...
+-- local testEffect4 = playdate.sound.delayline.new(0.05)
+
+-- for i=0, 5 do
+-- testEffect4:addTap(i * 0.01)
+-- end
+
+-- testEffect4:setFeedback(0.3)
+-- testEffect4:setMix(0)
+
+tapeEffect = CS16effect("tape", { tapeHiPass, tapeLoPass, tapeOverdrive }, { 1, 1, { 0.3, 0.8 } },
+ { "mix", "mix", "mix" },
+ 10)
+bitcrushEffect = CS16effect("btc", { bitcrushBitcrush, bitcrushBitcrush }, { { 0.6, 0.9 }, { 0.4, 0.85 } },
+ { "amount", "undersampling" }, 10)
+waterEffect = CS16effect("wtr", { waterLoPass, waterLoPass }, { { 1300, 800 }, 1 },
+ { "frequency", "mix" }, 10)
+overdriveEffect = CS16effect("ovd", { overdriveOverdrive, overdriveOverdrive, overdriveOverdrive }, { 1, 3, { 2, 1.2 } },
+ { "mix", "gain", "limit" }, 10)
+
+tapeEffect:notch(5)
+bitcrushEffect:notch(5)
+waterEffect:notch(5)
+overdriveEffect:notch(5)
+
+CS16effects = { tapeEffect, bitcrushEffect, waterEffect, overdriveEffect }
diff --git a/src/img/menu.png b/src/img/menu.png
index 864b55a..e89f257 100644
Binary files a/src/img/menu.png and b/src/img/menu.png differ
diff --git a/src/img/synthset.png b/src/img/synthset.png
index 6d7de95..c3a9e49 100644
Binary files a/src/img/synthset.png and b/src/img/synthset.png differ
diff --git a/src/lists.lua b/src/lists.lua
index 7cf9dff..69aef1a 100644
--- a/src/lists.lua
+++ b/src/lists.lua
@@ -3,20 +3,19 @@
listviewContents = {}
listview = pd.ui.gridview.new(0, 10)
-listview.backgroundImage = gfx.image.new(10,10,gfx.kColorWhite)
+listview.backgroundImage = gfx.image.new(10, 10, gfx.kColorWhite)
listview:setNumberOfRows(16)
listview:setCellPadding(0, 0, 5, 10)
listview:setContentInset(24, 24, 13, 11)
function listview:drawCell(section, row, column, selected, x, y, width, height)
-
if selected then
gfx.fillRoundRect(x, y, width, 20, 4)
gfx.setImageDrawMode(gfx.kDrawModeNXOR)
else
gfx.setImageDrawMode(gfx.kDrawModeNXOR)
end
- gfx.drawText(listviewContents[row], x+4, y+2, width, height, nil, "...", align.center)
+ gfx.drawText(listviewContents[row], x + 4, y + 2, width, height, nil, "...", align.center)
end
function listview:set(t)
@@ -27,7 +26,7 @@ end
filePickListContents = {}
filePickList = pd.ui.gridview.new(0, 10)
-filePickList.backgroundImage = gfx.image.new(10,10,gfx.kColorWhite)
+filePickList.backgroundImage = gfx.image.new(10, 10, gfx.kColorWhite)
filePickList:setNumberOfRows(1)
filePickList:setCellPadding(0, 0, 5, 10)
filePickList:setContentInset(24, 24, 13, 11)
@@ -43,7 +42,7 @@ function filePickList:drawCell(section, row, column, selected, x, y, width, heig
else
gfx.setImageDrawMode(gfx.kDrawModeNXOR)
end
- gfx.drawText(filePickListContents[row], x+4, y+2, width, height, nil, "...", align.center)
+ gfx.drawText(filePickListContents[row], x + 4, y + 2, width, height, nil, "...", align.center)
end
function filePickList:set(t)
@@ -55,11 +54,10 @@ function filePickList:get()
return filePickListContents
end
-
settingsListContents = {}
settingsList = pd.ui.gridview.new(0, 10)
-settingsList.backgroundImage = gfx.image.new(10,10,gfx.kColorWhite)
+settingsList.backgroundImage = gfx.image.new(10, 10, gfx.kColorWhite)
settingsList:setNumberOfRows(1)
settingsList:setCellPadding(0, 0, 5, 10)
settingsList:setContentInset(24, 24, 13, 11)
@@ -71,7 +69,7 @@ function settingsList:drawCell(section, row, column, selected, x, y, width, heig
else
gfx.setImageDrawMode(gfx.kDrawModeNXOR)
end
- gfx.drawText(settingsListContents[row], x+4, y+2, width, height, nil, "...", align.center)
+ gfx.drawText(settingsListContents[row], x + 4, y + 2, width, height, nil, "...", align.center)
end
function settingsList:set(t)
@@ -82,4 +80,3 @@ end
function settingsList:get()
return settingsListContents
end
-
diff --git a/src/main.lua b/src/main.lua
index 0301bcf..ab06ae7 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -10,7 +10,7 @@ import "CoreLibs/timer"
import "CoreLibs/keyboard"
import "CoreLibs/animator"
-stepCount = 32
+stepCount = 32 * 8
local firstTime = false
@@ -18,14 +18,17 @@ if not playdate.file.exists("settings.json") then
firstTime = true
end
+import "draw"
import "funcs"
+import "save"
+import "fx"
import "buttons"
import "consts"
import "setup"
import "lists"
import "ui"
-sinetimer = pd.timer.new(400-getTempoFromSPS(seq:getTempo()))
+sinetimer = pd.timer.new(400 - (getTempoFromSPS(seq:getTempo()) / 8))
sinetimer.repeats = true
songdir = "temp/"
@@ -34,21 +37,23 @@ pd.file.mkdir("samples")
pd.file.mkdir("songs")
pd.file.mkdir("temp")
-marker = {0,0}
-cursor = {0,0}
-trackNames = {"sin","squ","saw","tri","nse","poP","poD","poV","sin","squ","saw","tri","nse","poP","poD","poV"}
+marker = { 0, 0 }
+cursor = { 0, 0 }
+trackNames = { "sin", "squ", "saw", "tri", "nse", "poP", "poD", "poV", "sin", "squ", "saw", "tri", "nse", "poP", "poD", "poV" }
+userTrackNames = {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}
crankModes = crankModesList[1]
crankMode = "note status"
tempo = 120.0
selectedTrack = tracks[1]
textTimer = nil
textTimerText = ""
-screenModes = {"pattern","track","song"}
+screenModes = { "pattern", "track", "fx", "song" }
screenMode = "pattern"
currentElem = 1
autonote = "none"
songAuthor = settings["author"]
elementAnimator = gfx.animator.new(0, 0, 0)
+inScreen = false
pd.display.setInverted(settings["dark"])
@@ -57,7 +62,7 @@ pd.inputHandlers.push(pattern, true)
local finalListViewContents = {}
for i = 1, #trackNames, 1 do
- table.insert(finalListViewContents, tostring(i).." - "..trackNames[i])
+ table.insert(finalListViewContents, tostring(i) .. " - " .. trackNames[i])
end
listview:set(finalListViewContents)
@@ -67,13 +72,18 @@ pdmenu = pd.getSystemMenu()
applyMenuItems("pattern")
-knobs = {Knob(55,65,8,true),Knob(150,65,21),Knob(180,65,21),Knob(210,65,21),Knob(240,65,21),Knob(215,135,11),Knob(255,135,11),Knob(336,135,25,true),Knob(55,205,11)}
-buttons = {Button(5,5,nil,nil,"back",true),Button(310,55,nil,nil,"toggle",true),Button(53-(fnt8x8:getTextWidth("select")/2),125,nil,nil,"select",true),Button(127,125,nil,nil,"play",true)}
-
-allElems = {buttons[1],knobs[1],knobs[2],knobs[3],knobs[4],knobs[5],buttons[2],buttons[3],buttons[4],knobs[6],knobs[7],knobs[8],knobs[9]}
+knobs = { Knob(55, 65, 8, true), Knob(150, 65, 21), Knob(180, 65, 21), Knob(210, 65, 21), Knob(240, 65, 21), Knob(215,
+ 135, 11), Knob(255, 135, 11), Knob(336, 135, 25, true), Knob(55, 205, 11) }
+buttons = { Button(5, 5, nil, nil, "back", true), Button(333 - (fnt8x8:getTextWidth("toggle") / 2), 53, nil, nil,
+ "toggle", true), Button(53 - (fnt8x8:getTextWidth("select") / 2), 125, nil, nil, "select", true), Button(
+143 - (fnt8x8:getTextWidth("play") / 2), 125, nil, nil, "play", true) }
+allElems = { buttons[1], knobs[1], knobs[2], knobs[3], knobs[4], knobs[5], buttons[2], buttons[3], buttons[4], knobs[6],
+ knobs[7], knobs[8], knobs[9] }
function pd.update()
+ pd.timer.updateTimers()
+
local crank = 0
if crankMode == "tempo" then
crank = pd.getCrankTicks(8)
@@ -94,31 +104,58 @@ function pd.update()
end
if crankMode == "note status" then -- no "displayInfo" here because i thought it would get in the way
+ local thisTrack = table.indexOfElement(tracks, selectedTrack)
+
if autonote == "none" then
- toggleNote(getStepFromCursor(cursor))
+ local step = getStepFromCursor(cursor)
+ local swingVal = tracksSwingTable[thisTrack]
+
+ if step % 16 == 0 then
+ toggleNote(step + swingVal)
+ else
+ toggleNote(step)
+ end
else
- for i=1,stepCount do
- if (i-cursor[1]) % tonumber(autonote)-1 == 0 then
- toggleNote(i)
+ local mod = 0
+
+ if cursor[2] % 2 == 1 and autonote == "32" then
+ mod = 128
+ end
+
+ for i = 1, stepCount / 8 do
+ if (i - cursor[1]) % tonumber(autonote) - 1 == 0 then
+ toggleNote((i * 8) + mod)
elseif autonote == "1" then
- toggleNote(i)
+ toggleNote(i * 8)
end
end
end
+
+ applySwing(tracksSwingTable[thisTrack], selectedTrack, true)
+
+ updateInstsImage()
elseif crankMode == "pitch" or crankMode == "length" or crankMode == "velocity" then
local function applyNote(step)
- local note = selectedTrack:getNotes(step,step)
+ local swing
+
+ if step % 16 ~= 0 then
+ swing = 0
+ else
+ swing = tracksSwingTable[table.indexOfElement(tracks, selectedTrack)]
+ end
+
+ local note = selectedTrack:getNotes(step + swing, step + swing)
local concat = ""
if note[1] ~= nil then
- modifyNote(note[1],crankMode,crank,step)
- note = selectedTrack:getNotes(step,step)
+ modifyNote(note[1], crankMode, crank, step + swing)
+ note = selectedTrack:getNotes(step + swing, step + swing)
if crankMode == "pitch" then
- concat = "Ă "..MIDInotes[math.floor(note[1]["note"])-20]
+ concat = "*Ă* " .. MIDInotes[math.floor(note[1]["note"]) - 20]
elseif crankMode == "length" then
- concat = "len: "..note[1]["length"]
+ concat = "len: " .. math.floor(note[1]["length"] / 8)
elseif crankMode == "velocity" then
- concat = "vel: "..note[1]["velocity"]
+ concat = "vel: " .. note[1]["velocity"]
end
displayInfo(concat)
end
@@ -127,20 +164,32 @@ function pd.update()
if autonote == "none" then
applyNote(getStepFromCursor(cursor))
else
- for i=1,stepCount do
- if (i-cursor[1]) % tonumber(autonote)-1 == 0 then
- applyNote(i)
+ for i = 1, stepCount / 8 do
+ if (i - cursor[1]) % tonumber(autonote) - 1 == 0 then
+ applyNote(i * 8)
elseif autonote == "1" then
- applyNote(i)
+ applyNote(i * 8)
end
end
end
+
+ updateInstsImage()
+ elseif crankMode == "swing" then
+ local thisTrack = table.indexOfElement(tracks, selectedTrack)
+ local thisSwing = tracksSwingTable[thisTrack]
+
+ applySwing(thisSwing + crank, selectedTrack)
+
+ updateInstsImage()
+
+ displayInfo("swing: " .. tracksSwingTable[thisTrack])
elseif crankMode == "track" then
- selectedTrack = table.cycle(tracks,selectedTrack,b)
+ selectedTrack = table.cycle(tracks, selectedTrack, b)
+ updateInstsImage()
elseif crankMode == "screen" then
gfx.clear()
- crankModes = table.cycle(crankModesList,crankModes,b)
- screenMode = table.cycle(screenModes,screenMode,b)
+ crankModes = table.cycle(crankModesList, crankModes, b)
+ screenMode = table.cycle(screenModes, screenMode, b)
pd.inputHandlers.pop()
pdmenu:removeAllMenuItems()
@@ -151,59 +200,63 @@ function pd.update()
pd.inputHandlers.push(instrument, true)
elseif screenMode == "song" then
pd.inputHandlers.push(song, true)
+ elseif screenMode == "fx" then
+ pd.inputHandlers.push(fx, true)
end
applyMenuItems(screenMode)
local append = ""
if settings["num/max"] == true then
- append = " - "..table.find(screenModes,screenMode).."/"..#screenModes
+ append = " - " .. table.indexOfElement(screenModes, screenMode) .. "/" .. #screenModes
end
- displayInfo("screen: "..screenMode..append,750)
+ displayInfo("screen: " .. screenMode .. append, 750)
screenAnim(b)
elseif crankMode == "tempo" then
- seq:setTempo(math.max(2,math.min(64,seq:getTempo()+crank)))
- sinetimer.duration = 400-getTempoFromSPS(seq:getTempo())
+ seq:setTempo(math.max(16, math.min(512, seq:getTempo() + (crank * 8))))
+ sinetimer.duration = 400 - (getTempoFromSPS(seq:getTempo()) / 8)
if seq:isPlaying() and settings["stopontempo"] then
seq:stop()
seq:goToStep(1)
sinetimer:pause()
end
elseif crankMode == "pattern length" then
- local newThing = stepCount+(crank*16)
- if newThing <= 128 and newThing >= 16 then
+ local newThing = stepCount + ((crank * 16) * 8)
+ if newThing <= 128 * 8 and newThing >= 16 * 8 then
stepCount = newThing
- while (cursor[2]+1)*16 > stepCount do
+ while (cursor[2] + 1) * 16 > stepCount do
cursor[2] -= 1
end
end
- seq:setLoops(1,stepCount)
+ seq:setLoops(1, stepCount)
if not seq:isPlaying() then
sinetimer:pause()
end
+ updateStepsImage()
elseif crankMode == "turn knob" and listviewContents[#listviewContents] == "*Ā*" then
if allElems[currentElem]:isa(Knob) then
local selRow = listview:getSelectedRow()
local curKnob = allElems[currentElem]
- curKnob:click(1,crank)
+ curKnob:click(1, crank)
if currentElem == 2 then
if trackNames[selRow] == "smp" then
knobs[1]:setClicks(1)
end
- instrument.selectedInst:setWaveform(waveTable[(knobs[1]:getCurrentClick())+1])
- trackNames[selRow] = waveNames[(knobs[1]:getCurrentClick()+1)]
- displayInfo("wave: "..trackNames[selRow])
+ instrument.selectedInst:setWaveform(waveTable[(knobs[1]:getCurrentClick()) + 1])
+ trackNames[selRow] = waveNames[(knobs[1]:getCurrentClick() + 1)]
+
+ displayInfo("wave: " .. trackNames[selRow])
elseif currentElem < 7 then
- local adsrNames = {"attack: ","decay: ","sustain: ","release: "}
+ local adsrNames = { "attack: ", "decay: ", "sustain: ", "release: " }
local adsr = instrumentADSRtable[selRow]
- adsr[currentElem-2] = math.round(math.normalize((0.1*crank)+adsr[currentElem-2],0.0,2.0),1)
- instrument.selectedInst:setADSR(adsr[1],adsr[2],adsr[3],adsr[4])
- displayInfo(adsrNames[currentElem-2]..adsr[currentElem-2])
+ adsr[currentElem - 2] = math.round(math.normalize((0.1 * crank) + adsr[currentElem - 2], 0.0, 2.0), 1)
+ instrument.selectedInst:setADSR(adsr[1], adsr[2], adsr[3], adsr[4])
+ displayInfo(adsrNames[currentElem - 2] .. adsr[currentElem - 2])
elseif currentElem < 12 then
local paramNum
if currentElem == 10 then
@@ -212,64 +265,106 @@ function pd.update()
paramNum = 2
end
- instrumentParamTable[selRow][paramNum] = math.round(math.normalize((0.1*crank)+instrumentParamTable[selRow][paramNum],0.0,1.0),1)
+ instrumentParamTable[selRow][paramNum] = math.round(
+ math.normalize((0.1 * crank) + instrumentParamTable[selRow][paramNum], 0.0, 1.0), 1)
instrument.selectedInst:setParameter(paramNum, instrumentParamTable[selRow][paramNum])
- displayInfo("param "..paramNum..": "..instrumentParamTable[selRow][paramNum])
+ displayInfo("param " .. paramNum .. ": " .. instrumentParamTable[selRow][paramNum])
elseif currentElem == 12 then -- shift
local old = instrumentTransposeTable[selRow]
- instrumentTransposeTable[selRow] = math.normalize(instrumentTransposeTable[selRow]+1*crank,-24,24)
+ instrumentTransposeTable[selRow] = math.normalize(instrumentTransposeTable[selRow] + 1 * crank, -24, 24)
if old == instrumentTransposeTable[selRow] then
curKnob:click(-1, crank)
end
tracks[selRow]:getInstrument():setTranspose(instrumentTransposeTable[selRow])
- displayInfo("transpose: "..instrumentTransposeTable[selRow])
+ displayInfo("transpose: " .. instrumentTransposeTable[selRow])
else
local old = instrument.selectedInst:getVolume()
- local newVol = math.round(math.normalize((0.1*crank)+old,0.0,1.0),1)
+ local newVol = math.round(math.normalize((0.1 * crank) + old, 0.0, 1.0), 1)
instrument.selectedInst:setVolume(newVol)
- displayInfo("volume: "..newVol)
+ displayInfo("volume: " .. newVol)
+ end
+ end
+ elseif crankMode == "lock effect" then
+ for i, v in ipairs(CS16effects) do
+ if v:getEnabled() then
+ if v:getLocked() and crank == -1 then
+ v:setLocked(false)
+ v:disable()
+ else
+ v:setLocked(true)
+ end
end
end
+ elseif crankMode == "effect" then
+ local curEffect = CS16effects[fx.selectedEffect]
+ CS16effects[fx.selectedEffect]:setName(table.cycle(validEffectsNames, curEffect:getName()))
+ elseif crankMode == "effect intensity" then
+ if fx.selectedEffect ~= 0 then
+ CS16effects[fx.selectedEffect]:notch(crank)
+ end
end
end
- gfx.clear()
+ if screenMode ~= "track" then
+ gfx.clear()
+ end
if screenMode == "pattern" then -- all the drawing functions
- drawInsts()
drawSteps()
+ drawInsts()
+ drawNoteOn()
drawCursor()
if autonote ~= "none" then
- gfx.drawTextAligned("a*Ă*: "..autonote,400,222,align.right)
+ gfx.drawTextAligned("a*Ă*: " .. autonote, 400, 222, align.right)
end
- gfx.drawText(currentSeqStep,0,222)
- gfx.drawTextAligned(table.find(tracks,selectedTrack).."-"..trackNames[table.find(tracks,selectedTrack)],200,222,align.center)
+ local index = table.indexOfElement(tracks, selectedTrack)
+
+ local name = trackNames[index]
+
+ if userTrackNames[index] ~= "" then
+ name = userTrackNames[index]
+ end
+ gfx.drawText(math.ceil(currentSeqStep / 8), 0, 222)
+ gfx.drawTextAligned(
+ table.indexOfElement(tracks, selectedTrack) .. "-" .. name, 200,
+ 222, align.center)
elseif screenMode == "track" then
local selRow = listview:getSelectedRow()
if listviewContents[1] ~= "*Ā*" then
- listview:drawInRect(0, 0, 400, 240)
- --fnt8x8:drawTextAligned("tracks",200,0,align.center)
- if instrument.allMuted == true then
- gfx.drawText("*ć*",383,223)
+ if listview.needsDisplay or textTimer.timeLeft ~= 0 then
+ listview:drawInRect(0, 0, 400, 240)
+
+ if instrument.allMuted == true then
+ gfx.drawText("*ć*", 383, 223)
+ end
+
+ -- fnt8x8:drawTextAligned("tracks",200,0,align.center)
end
else
- synthset:draw(0,0)
+ local concatTable = {selRow, "-"}
+
+ gfx.clear()
+ synthset:draw(0, 0)
+
+ gfx.setLineWidth(3)
if trackNames[selRow] == "smp" then
- gfx.drawLine(92,125,101,125)
- gfx.drawLine(101,125,101,55)
- gfx.drawLine(101,55,120,55)
+ gfx.drawLine(92, 125, 103, 125)
+ gfx.drawLine(101, 125, 101, 54)
+ gfx.drawLine(101, 55, 120, 55)
else
- gfx.drawLine(92,55,120,55)
+ gfx.drawLine(92, 55, 120, 55)
end
+ gfx.setLineWidth(1)
+
if instrumentLegatoTable[selRow] == true then
- gfx.fillCircleAtPoint(336,76,5)
+ gfx.fillCircleAtPoint(336, 76, 2)
end
for i = 1, #allElems, 1 do
@@ -285,84 +380,286 @@ function pd.update()
allElems[i]:draw(sel, prs)
end
- fnt8x8:drawTextAligned(selRow.."-"..trackNames[selRow],400,231,align.right)
+ local name = trackNames[selRow]
+
+ if userTrackNames[selRow] ~= "" then
+ name = userTrackNames[selRow]
+ end
+
+ table.insert(concatTable, name)
+
+ if userTrackNames[selRow] ~= "" then
+ table.insert(concatTable, " (" .. trackNames[selRow] .. ")")
+ end
+
+ fnt8x8:drawTextAligned(table.concat(concatTable), 400, 231, align.right)
end
elseif screenMode == "song" then
- local curStep = seq:getCurrentStep()
- local metronome = {"*Ĉ*", "*ĉ*", "*Ċ*", "*ĉ*"}
+ local curStep = seq:getCurrentStep() / 8
+ local metronome = {"*ĉ*", "*Ċ*", "*ĉ*", "*Ĉ*"}
local curMet = metronome[1]
- if (curStep - 1) % 4 < 4 then
+ if not seq:isPlaying() then
+ curMet = metronome[1]
+ elseif (curStep) % 4 < 4 then
curMet = metronome[math.ceil(curStep / 4) % 4 + 1]
end
local toDraw = "no name"
if songdir ~= "temp/" then
- toDraw = string.normalize(string.split(songdir,"/")[#string.split(songdir,"/")-1])
+ toDraw = string.normalize(string.split(songdir, "/")[#string.split(songdir, "/") - 1])
end
- gfx.drawTextInRect(toDraw.." by "..songAuthor,0,0,400,240,nil,nil,align.center)
+ gfx.drawTextInRect(toDraw .. " by " .. songAuthor, 0, 0, 400, 240, nil, nil, align.center)
- if settings["visualizer"] >= 2 then
+ gfx.drawTextAligned(curMet .. (getTempoFromSPS(seq:getTempo() / 8)), 400, 222, align.right)
+ gfx.drawText(math.floor(stepCount / 8) .. " steps", 0, 222)
+
+
+ if settings["visualizer"]["sine"] then -- sine wave
gfx.setLineWidth(2)
- gfx.drawSineWave(0,120,405,120,stepCount/2,stepCount/2,math.max(10,400-getTempoFromSPS(seq:getTempo())),sinetimer.currentTime)
+ gfx.drawSineWave(0, 120, 405, 120, (stepCount / 2) / 8, (stepCount / 2) / 8,
+ math.max(10, 400 - (getTempoFromSPS(seq:getTempo()) / 8)),
+ sinetimer.currentTime)
gfx.setLineWidth(1)
end
- if settings["visualizer"] == 1 or settings["visualizer"] == 3 then
- for i=1, 16 do
+ if settings["visualizer"]["notes"] then -- note status display
+ for i = 1, 16 do
if instrumentTable[i]:isPlaying() then
gfx.setColor(gfx.kColorXOR)
- gfx.fillRoundRect((i*25)-22,110,20,20,2)
+ gfx.fillRoundRect((i * 25) - 22, 110, 20, 20, 2)
gfx.setColor(gfx.kColorBlack)
end
end
end
- gfx.drawTextAligned(curMet..getTempoFromSPS(seq:getTempo()), 400, 222, align.right)
- gfx.drawText(stepCount.." steps", 0, 222)
- end
+ if settings["visualizer"]["stars"] then -- stars!
+ for i, v in ipairs(visualizerStars) do
+ v:update()
+ end
+ end
+
+ if #externalVisualizers > 0 then -- all external visualizers
+ local isBeat = false
+
+ if ((math.round(curStep)) % 8 == 1) and seq:isPlaying() then
+ isBeat = true
+ end
+
+ local data = { -- thankfully most of these are just pointers haha, it would suck if i had to .deepcopy() them or something
+ tempo=tempo,
+ step=math.round(curStep),
+ rawStep=curStep,
+ length=stepCount,
+ playing=seq:isPlaying(),
+ beat=isBeat,
+ tracks=tracks,
+ trackNames=trackNames,
+ userTrackNames=userTrackNames,
+ trackSwings=tracksSwingTable,
+ mutedTracks=tracksMutedTable,
+ instruments=instrumentTable,
+ instrumentADSRs=instrumentADSRtable,
+ instrumentLegatos=instrumentLegatoTable,
+ instrumentParams=instrumentParamTable,
+ instrumentTransposes=instrumentTransposeTable,
+ settings=settings,
+ sequencer=seq
+ }
+
+ for i, v in ipairs(externalVisualizers) do
+ if settings["visualizer"][v[1]] then
+ v[2](data)
+ end
+ end
+ end
+ elseif screenMode == "fx" then
+ gfx.clear()
+
+ drawFxTriangle(200, 40, "n", 30, tapeEffect:getEnabled())
+ drawFxTriangle(200, 200, "s", 30, waterEffect:getEnabled())
+ drawFxTriangle(280, 120, "e", 30, bitcrushEffect:getEnabled())
+ drawFxTriangle(120, 120, "w", 30, overdriveEffect:getEnabled())
+
+ if fx.enabled then
+ -- gfx.getSystemFont():getGlyph("Ⓐ"):scaledImage(2):drawCentered(202, 122)
+ gfx.drawTextAligned("ACTV!", 200, 112, kTextAlignment.center)
+ end
+
+ local effectIsLocked = false
+
+ for i, v in ipairs(CS16effects) do
+ if v:getLocked() == true then
+ effectIsLocked = true
+ break
+ end
+ end
+
+ if effectIsLocked then
+ gfx.drawText("*Ć*", 0, 224)
+ end
+
+ if fx.selectedEffect ~= 0 then
+ local tw, th
+ local spacer = " - "
+
+ if fx.selectedEffect % 2 == 0 then
+ spacer = "\n"
+ end
+
+ tw, th = gfx.getTextSize(CS16effects[fx.selectedEffect]:getName() ..
+ spacer .. CS16effects[fx.selectedEffect]:getOverallValue())
+
+ local Xs = { 200, 340, 200, 60 }
+ local Ys = { 10, 103, 212, 103 }
+
+ gfx.drawRect((Xs[fx.selectedEffect] - (tw / 2)) - 4, Ys[fx.selectedEffect] - 2, tw + 10, th + 6)
+ end
+
+ gfx.drawTextAligned(tapeEffect:getName() .. " - " .. tapeEffect:getOverallValue(), 200, 10, kTextAlignment.center)
+ gfx.drawTextAligned(bitcrushEffect:getName() .. "\n" .. bitcrushEffect:getOverallValue(), 340, 103,
+ kTextAlignment.center)
+ gfx.drawTextAligned(waterEffect:getName() .. " - " .. waterEffect:getOverallValue(), 200, 212, kTextAlignment.center)
+ gfx.drawTextAligned(overdriveEffect:getName() .. "\n" .. overdriveEffect:getOverallValue(), 60, 103,
+ kTextAlignment.center)
+
+ if settings["fxvfx"] then
+ if tapeEffect:getEnabled() then
+ local curImg = gfx.getWorkingImage()
+ gfx.clear()
+ curImg:vcrPauseFilterImage():draw(0,0)
+ gfx.pushContext()
+ gfx.setColor(gfx.kColorWhite)
+ local w, _ = gfx.getTextSize("no signal")
+ gfx.fillRect(10, 10, w + 2, 18)
+ gfx.drawText("no signal", 10, 10)
+ gfx.popContext()
+ end
+
+ if bitcrushEffect:getEnabled() or overdriveEffect:getEnabled() then
+ local intensity = 1
+ if overdriveEffect:getEnabled() then
+ intensity = overdriveEffect:getOverallValue() * 5
+ else
+ intensity = bitcrushEffect:getOverallValue() * 3
+ end
+
+ intensity = math.round(intensity)
+
+ pd.display.setOffset(math.random(-intensity, intensity), math.random(-intensity, intensity))
+
+ if not pd.getReduceFlashing() then
+ if bitcrushEffect:getEnabled() then
+ gfx.drawText("reboot", math.random(-100, 400), math.random(-10, 240))
+ gfx.drawText("warning", math.random(-100, 400), math.random(-10, 240))
+ gfx.drawText("deprecated", math.random(-100, 400), math.random(-10, 240))
+ end
+
+ if overdriveEffect:getEnabled() then
+ gfx.drawText("error", math.random(-100, 400), math.random(-10, 240))
+ gfx.drawText("corefault", math.random(-100, 400), math.random(-10, 240))
+ gfx.drawText("!!", math.random(-100, 400), math.random(-10, 240))
+ end
+ end
+ else
+ pd.display.setOffset(0, 0)
+ end
+ if waterEffect:getEnabled() then
+ local curImg = gfx.getWorkingImage():copy()
+ gfx.clear()
+ curImg:drawBlurred(0, 0, math.round(waterEffect:getOverallValue() * 3), 1, gfx.image.kDitherTypeBayer4x4)
+
+ local w, _ = gfx.getTextSize("unstable")
+ gfx.pushContext()
+ gfx.setColor(gfx.kColorWhite)
+ gfx.fillRect(200 - (w / 2), 112, w + 2, 18)
+ gfx.drawTextAligned("unstable", 200, 112, kTextAlignment.center)
+ gfx.popContext()
+ end
+ end
+ end
+
if elementAnimator:ended() == false then
- pd.display.setOffset(elementAnimator:currentValue(),0)
+ pd.display.setOffset(elementAnimator:currentValue(), 0)
end
if textTimer ~= nil and textTimer.active == true then
- local sizex,sizey = gfx.getTextSize(textTimerText)
+ local sizex, sizey = gfx.getTextSize(textTimerText)
local offsetx, offsety = pd.display.getOffset()
- local rectx,recty,rectw,recth = (200-(sizex/2))-2-offsetx,109,sizex+6,sizey+6
-
- gfx.fillRect(rectx-2,recty-2,rectw,recth)
+ local rectx, recty, rectw, recth = (200 - (sizex / 2)) - 2 - offsetx, 109, sizex + 6, sizey + 6
+
+ gfx.fillRect(rectx - 2, recty - 2, rectw, recth)
gfx.setColor(gfx.kColorWhite)
- gfx.fillRect(rectx,recty,rectw,recth)
+ gfx.fillRect(rectx, recty, rectw, recth)
gfx.setColor(gfx.kColorBlack)
- gfx.drawRect(rectx,recty,rectw,recth)
- gfx.drawTextAligned(textTimerText,200-offsetx,111,align.center)
+ gfx.drawRect(rectx, recty, rectw, recth)
+ gfx.drawTextAligned(textTimerText, 200 - offsetx, 111, align.center)
end
- pd.timer.updateTimers()
--pd.drawFPS(0,0)
end
+function pd.gameWillPause()
+ pd.display.setOffset(0, 0)
+end
+
function pd.gameWillTerminate()
- pd.file.delete("temp/",true)
+ local time = pd.getTime()
+
+ local finalRemarks = {
+ "this machine is now entering idle mode...",
+ "shutdown completed at " .. time.hour .. ":" .. time.minute .. ", " .. time.month .. "/" .. time.day .. "/2634",
+ "0 defective items found.",
+ "sending " .. math.random(2, 15) .. " defects to incinerator...",
+ "REMINDER: assembly line at DWZ331 requires attention.",
+ "giving payouts to organic lifeforms...",
+ "sending " .. songAuthor .. " back to timeline 28731...",
+ "please report to iwp://xythia.optou/konoye."
+ }
+
+ local log = logScreen
+
+ log.init()
+
+ log.append("shutting down cs-16 v" .. pd.metadata.version)
+ log.append("deleting temp directory...")
+ pd.file.delete("temp/", true)
+ log.append(finalRemarks[math.random(#finalRemarks)])
+ log.append("returning to launcher. bye!")
end
function pd.crankDocked()
- screenMode = "pattern"
- pdmenu:removeAllMenuItems()
- crankModes = crankModesList[1]
- pd.inputHandlers.pop()
- pd.inputHandlers.push(pattern, true)
- applyMenuItems("pattern")
+ if settings["crankDockedScreen"] ~= "none" and not inScreen then
+ screenMode = settings["crankDockedScreen"]
+ pdmenu:removeAllMenuItems()
+ crankMode = "screen"
+ crankModes = crankModesList[table.indexOfElement(screenModes, screenMode)]
+ pd.inputHandlers.pop()
+
+ if screenMode == "pattern" then
+ pd.inputHandlers.push(pattern, true)
+ elseif screenMode == "track" then
+ instrument.copytrack = 0
+ pd.inputHandlers.push(instrument, true)
+ instrument.updateList()
+ elseif screenMode == "fx" then
+ pd.inputHandlers.push(fx, true)
+ elseif screenMode == "song" then
+ pd.inputHandlers.push(song, true)
+ end
+
+ applyMenuItems(screenMode)
+ end
end
if settings["playonload"] == true then
seq:play()
end
-snd.getHeadphoneState(function(phones,mic)
- local hp,spk = false, false
+snd.getHeadphoneState(function(phones, mic)
+ local hp, spk = false, false
local text = "headphones plugged in"
if settings["output"] == "auto" then
if phones == true then
@@ -380,8 +677,9 @@ snd.getHeadphoneState(function(phones,mic)
end
end)
-displayInfo("cs-16 v"..pd.metadata.version,2000)
+displayInfo("cs-16 v" .. pd.metadata.version, 2000)
if firstTime then -- reading the manual is pretty important, so i thought this would be helpful.
- messageBox.open("welcome to cs-16! :)\n\nbefore you begin, it is highly recommended that you read the manual, as most functions are not immediately apparent and are hard to reach without aid from the documentation.\n\nyou can read it at https://is.gd/cs16m/ (all capital letters).\n\npress a to continue.")
+ messageBox.open(
+ "welcome to cs-16! :)\n\nbefore you begin, it is highly recommended that you read the manual, as most functions are not immediately apparent and are hard to reach without aid from the documentation.\n\nscan the qr code in the system menu to view it!\n\npress a to continue.")
end
diff --git a/src/pdxinfo b/src/pdxinfo
index f6acaa5..faa0f38 100644
--- a/src/pdxinfo
+++ b/src/pdxinfo
@@ -2,6 +2,6 @@ name=CS-16
author=nanobot567
description=cranky synth 16: a 16-track synthesizer/sampler for playdate.
bundleID=com.nano.cs16
-version=1.3
-buildNumber=3086
+version=2.0
+buildNumber=4582
imagePath=SystemAssets/
diff --git a/src/save.lua b/src/save.lua
new file mode 100644
index 0000000..5fbec1b
--- /dev/null
+++ b/src/save.lua
@@ -0,0 +1,232 @@
+-- saving to disk functions
+
+function buildSave(name)
+ print("saving song to " .. name)
+ pd.file.mkdir("songs/" .. name)
+ -- format
+ --
+ -- 1 = tracks
+ -- |- 1 = name
+ -- |- 2 = notes
+ -- |- 3 = adsr
+ -- |- 4 = legato
+ -- |- 5 = params
+ -- |- 6 = transpose
+ -- |- 7 = volume
+ -- |- 8 = swing
+ -- -- 9 = user track name
+ -- 2 = other
+ -- |- 1 = tempo
+ -- -- 2 = steps
+ -- 3 = metadata
+ -- |- 1 = author
+ -- -- 2 = time and date
+
+ local tmp = { { {}, {}, {}, {}, {}, {}, {}, {}, {} }, {}, {} }
+
+ for i, v in ipairs(tracks) do
+ tmp[1][1][i] = trackNames[i]
+ tmp[1][2][i] = v:getNotes()
+ tmp[1][3][i] = instrumentADSRtable[i]
+ tmp[1][4][i] = instrumentLegatoTable[i]
+ tmp[1][5][i] = instrumentParamTable[i]
+ tmp[1][6][i] = instrumentTransposeTable[i]
+ tmp[1][7][i] = instrumentTable[i]:getVolume()
+ tmp[1][8][i] = tracksSwingTable[i]
+ tmp[1][9][i] = userTrackNames[i]
+ end
+
+ for i, v in ipairs(pd.file.listFiles("temp/")) do
+ if string.sub(v, #v - 2) == "pda" or string.sub(v, #v - 2) == "wav" then
+ local smp, err = snd.sample.new("temp/" .. v)
+ if err ~= nil then
+ print(err)
+ end
+ smp:save("songs/" .. name .. "/" .. v)
+ elseif string.sub(v, #v - 2) == "pdi" then
+ pd.datastore.writeImage(pd.datastore.readImage("temp/" .. v), "songs/" .. name .. "/" .. v)
+ end
+ end
+
+ tmp[2][1] = seq:getTempo() / 8
+ tmp[2][2] = stepCount / 8
+
+ tmp[3][1] = settings["author"]
+ tmp[3][2] = pd.getTime()
+
+ pd.datastore.write(tmp, "songs/" .. name .. "/song", false)
+ print("success!")
+ return tmp
+end
+
+function loadSave(name)
+ local log = logScreen
+ log.init()
+
+ log.append("cs-16 v" .. pd.metadata.version .. " ptfs loader")
+ log.append("")
+ log.append("stopping sequencer...")
+
+ seq:stop()
+
+ log.append("stopping active voices...")
+
+ seq:allNotesOff()
+ seq:goToStep(1)
+
+ log.append("clearing temp dir...")
+
+ pd.file.delete("temp/", true)
+ pd.file.mkdir("temp/")
+
+ log.append("loading from " .. name .. "...")
+
+ print("loading song from " .. name)
+ local tmp = pd.datastore.read(name .. "song")
+
+ log.append("applying track data...")
+
+ for i, v in ipairs(tracks) do
+ log.append("applying data for track " .. i .. "...")
+
+ trackNames[i] = tmp[1][1][i]
+
+ if tmp[1][8] == nil then
+ for k, noteVal in pairs(tmp[1][2][i]) do
+ tmp[1][2][i][k]["step"] = noteVal["step"] * 8
+ tmp[1][2][i][k]["length"] = noteVal["length"] * 8
+ end
+ end
+
+ v:setNotes(tmp[1][2][i])
+
+ instrumentADSRtable[i] = tmp[1][3][i]
+ local adsr = instrumentADSRtable[i]
+ instrumentTable[i]:setADSR(adsr[1], adsr[2], adsr[3], adsr[4])
+
+ if trackNames[i] ~= "smp" then
+ log.append("loading waveform " .. trackNames[i] .. "...")
+
+ instrumentTable[i]:setWaveform(waveTable[table.indexOfElement(waveNames, trackNames[i])])
+ else
+ log.append("loading sample...")
+
+ local smp = snd.sample.new(name .. i .. ".pda")
+ instrumentTable[i]:setWaveform(WAVE_SIN)
+ instrumentTable[i]:setWaveform(smp)
+ smp:save("temp/" .. i .. ".pda")
+
+ local sampleImage = pd.datastore.readImage(name .. i .. ".pdi")
+ if sampleImage ~= nil then
+ log.append("duplicating sample image...")
+ pd.datastore.writeImage(sampleImage, "temp/" .. i .. ".pdi")
+ end
+
+ if settings["savewavs"] == true then
+ smp:save("temp/" .. i .. ".wav")
+ end
+ end
+
+ log.append("applying modifiers...")
+
+ instrumentLegatoTable[i] = tmp[1][4][i]
+ instrumentTable[i]:setLegato(instrumentLegatoTable[i])
+ instrumentParamTable[i] = tmp[1][5][i]
+ instrumentTable[i]:setParameter(1, instrumentParamTable[i][1])
+ instrumentTable[i]:setParameter(2, instrumentParamTable[i][2])
+ instrumentTransposeTable[i] = tmp[1][6][i]
+ tracks[i]:getInstrument():setTranspose(instrumentTransposeTable[i])
+ if tmp[1][7] then
+ instrumentTable[i]:setVolume(tmp[1][7][i])
+ end
+ tracksMutedTable[i] = false
+ tracks[i]:setMuted(false)
+ instrument.allMuted = false
+
+ if tmp[1][8] then
+ log.append("found swing, applying...")
+
+ tracksSwingTable[i] = tmp[1][8][i]
+ applySwing(tracksSwingTable[i], tracks[i], true)
+ end
+
+ if tmp[1][9] then
+ if tmp[1][9][i] ~= "" then
+ log.append("found custom track name, applying...")
+ end
+ userTrackNames[i] = tmp[1][9][i]
+ else
+ userTrackNames[i] = ""
+ end
+
+ for i = 1, 16 do
+ instrumentTable[i]:stop()
+ end
+
+ local finalListViewContents = {}
+
+ for i = 1, #trackNames, 1 do
+ table.insert(finalListViewContents, tostring(i) .. " - " .. trackNames[i])
+ end
+
+ listview:set(finalListViewContents)
+ end
+
+ log.append("setting tempo to " .. tmp[2][1] .. "...")
+
+ seq:setTempo(tmp[2][1] * 8) -- TODO: be sure to implement real world bpm changes once fix is pushed!
+ sinetimer.duration = 400 - (getTempoFromSPS(seq:getTempo()) / 8)
+
+ log.append("setting stepcount...")
+
+ stepCount = tmp[2][2] * 8
+ seq:setLoops(1, stepCount)
+
+ songAuthor = tmp[3][1]
+
+ log.append("finalizing...")
+
+ updateStepsImage()
+ updateInstsImage()
+ instrument.updateList()
+
+ cursor = { 0, 0 }
+ crankMode = "screen"
+ print("success!")
+
+ log.append("success!")
+end
+
+function saveSettings()
+ pd.datastore.write(settings, "settings")
+end
+
+function loadSettings()
+ local data = pd.datastore.read("settings")
+ if data ~= nil then
+ for k, v in pairs(settings) do
+ if data[k] == nil or type(data[k]) ~= type(v) then
+ data[k] = v
+ end
+ end
+
+ if data["50fps"] == true then
+ pd.display.setRefreshRate(50)
+ else
+ pd.display.setRefreshRate(30)
+ end
+
+ if data["useSystemFont"] == true then
+ fnt = rains2x
+ fnt8x8 = rains1x
+ else
+ fnt = gfx.font.new("fnt/modified-tron")
+ fnt8x8 = gfx.font.new("fnt/modified-tron-8x8")
+ end
+
+ gfx.setFont(fnt)
+
+ return data
+ end
+ return settings
+end
diff --git a/src/setup.lua b/src/setup.lua
index ee538e0..a27ec8c 100644
--- a/src/setup.lua
+++ b/src/setup.lua
@@ -1,31 +1,38 @@
-knobRotations = {0}
-crankSensList = {1,2,3,4,5,6,7,8}
+knobRotations = { 0 }
+crankSensList = { 1, 2, 3, 4, 5, 6, 7, 8 }
settings = {
- ["dark"]=true,
- ["playonload"]=true,
- ["cranksens"]=4,
- ["author"]="anonymous",
- ["output"]=3,
- ["stoponsample"]=true,
- ["stopontempo"]=true,
- ["savewavs"]=false,
- ["visualizer"]=3,
- ["pmode"]=false,
- ["num/max"]=true,
+ ["dark"] = true,
+ ["playonload"] = true,
+ ["cranksens"] = 4,
+ ["author"] = "anonymous",
+ ["output"] = 3,
+ ["stoponsample"] = true,
+ ["stopontempo"] = true,
+ ["savewavs"] = false,
+ ["visualizer"] = {
+ sine=false,
+ notes=true,
+ stars=true
+ },
+ ["50fps"] = false,
+ ["num/max"] = true,
-- button mapping for recording
- ["aRecTrack"]=2,
- ["bRecTrack"]=1,
- ["upRecTrack"]=3,
- ["downRecTrack"]=5,
- ["leftRecTrack"]=6,
- ["rightRecTrack"]=4,
- ["recordQuantization"]=1,
- ["sample16bit"]=true,
- ["showNoteNames"]=true,
- ["useSystemFont"]=false,
- ["saveWaveforms"]=false,
- ["screenAnimation"]=true
+ ["aRecTrack"] = 2,
+ ["bRecTrack"] = 1,
+ ["upRecTrack"] = 3,
+ ["downRecTrack"] = 5,
+ ["leftRecTrack"] = 6,
+ ["rightRecTrack"] = 4,
+ ["recordQuantization"] = 1,
+ ["sample16bit"] = true,
+ ["showNoteNames"] = true,
+ ["useSystemFont"] = false,
+ ["saveWaveforms"] = false,
+ ["screenAnimation"] = true,
+ ["logscreens"] = true,
+ ["fxvfx"] = false,
+ ["crankDockedScreen"] = "pattern"
}
settings = loadSettings()
saveSettings()
@@ -34,11 +41,11 @@ metronomeTrack = snd.track.new()
local metronome = synth.new(WAVE_SQU)
metronome:setVolume(0.1)
metronomeTrack:setInstrument(metronome)
-for i=1, 128 do
+for i = 1, 128 do
if i == 1 then
- metronomeTrack:addNote(i, "C6", 1)
+ metronomeTrack:addNote(i * 8, "C6", 1)
elseif i % 8 == 1 then
- metronomeTrack:addNote(i, "C5", 1)
+ metronomeTrack:addNote(i * 8, "C5", 1)
end
end
@@ -61,49 +68,90 @@ local i14 = synth.new(WAVE_POP)
local i15 = synth.new(WAVE_POD)
local i16 = synth.new(WAVE_POV)
-instrumentTable = {i1,i2,i3,i4,i5,i6,i7,i8,i9,i10,i11,i12,i13,i14,i15,i16}
+instrumentTable = { i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16 }
tracksMutedTable = {}
instrumentADSRtable = {}
instrumentLegatoTable = {}
instrumentParamTable = {}
instrumentTransposeTable = {}
-channelTable = {}
tracks = {}
+tracksSwingTable = {}
-for i=1, 16 do
- table.insert(instrumentADSRtable,{0,0,0.3,0.4})
+for i = 1, 16 do
+ table.insert(instrumentADSRtable, { 0, 0, 0.3, 0.4 })
local adsr = instrumentADSRtable[i]
- instrumentTable[i]:setADSR(adsr[1],adsr[2],adsr[3],adsr[4])
+ instrumentTable[i]:setADSR(adsr[1], adsr[2], adsr[3], adsr[4])
instrumentTable[i]:setEnvelopeCurvature(0.5) -- probably should add a setting to change this...
instrumentLegatoTable[i] = false
instrumentTransposeTable[i] = 0
if i == 2 or i == 10 then
- instrumentParamTable[i] = {0.5,0.0}
+ instrumentParamTable[i] = { 0.5, 0.0 }
else
- instrumentParamTable[i] = {0.0,0.0}
+ instrumentParamTable[i] = { 0.0, 0.0 }
end
table.insert(tracks, snd.track.new())
tracks[i]:setInstrument(instrumentTable[i])
tracksMutedTable[i] = false
+
+ tracksSwingTable[i] = 0
end
-waveTable = {WAVE_SIN,WAVE_SQU,WAVE_SAW,WAVE_TRI,WAVE_NSE,WAVE_POP,WAVE_POD,WAVE_POV} -- haha funni joke wavetable
-waveNames = {"sin","squ","saw","tri","nse","poP","poD","poV"}
+waveTable = { WAVE_SIN, WAVE_SQU, WAVE_SAW, WAVE_TRI, WAVE_NSE, WAVE_POP, WAVE_POD, WAVE_POV } -- haha funni joke wavetable
+waveNames = { "sin", "squ", "saw", "tri", "nse", "poP", "poD", "poV" }
seq = snd.sequence.new()
-seq:setLoops(1,stepCount)
-for i=1, #tracks do
+seq:setLoops(1, stepCount)
+for i = 1, #tracks do
seq:addTrack(tracks[i])
end
seq:addTrack(metronomeTrack)
-
+seq:setTempo(128)
seq:play()
pd.setMenuImage(gfx.image.new("img/menu"))
pd.setCrankSoundsDisabled(true)
gfx.setImageDrawMode(gfx.kDrawModeNXOR)
+
+currentInstsImage = gfx.image.new(400, 240, gfx.kColorClear)
+currentStepsImage = gfx.image.new(400, 240, gfx.kColorClear)
+
+updateStepsImage()
+
+
+visualizerStars = {}
+
+for i = 1, 12, 1 do
+ table.insert(visualizerStars, Particle())
+end
+
+-- visualizer api
+
+externalVisualizers = {}
+
+local visualizerContent
+
+local files = pd.file.listFiles("visualizers")
+
+if files then
+ for i, v in ipairs(files) do
+ if pd.file.isdir("visualizers/" .. v) then
+ for insideFolderIndex, insideFolderItem in ipairs(pd.file.listFiles("visualizers/" .. v)) do
+ if string.sub(insideFolderItem, #insideFolderItem-3) == ".pdz" then
+ visualizerContent = pd.file.run("visualizers/" .. v .. insideFolderItem)
+ table.insert(externalVisualizers, visualizerContent)
+ break
+ end
+ end
+ else
+ if string.sub(v, #v-3) == ".pdz" then
+ visualizerContent = pd.file.run("visualizers/" .. v)
+ table.insert(externalVisualizers, visualizerContent)
+ end
+ end
+ end
+end
diff --git a/src/ui.lua b/src/ui.lua
index 3eb7ea0..05b1df0 100644
--- a/src/ui.lua
+++ b/src/ui.lua
@@ -1,1087 +1,10 @@
--- ui classes and sub-screens (keyboard screen, settings screen, etc. basically everything else that uses inputHandlers.)
-
-class("Knob").extends()
-class("Button").extends()
-
-function Knob:init(x, y, clicks, rot)
- self.x = x
- self.y = y
- self.clicks = 360/clicks
- self.rotation = 0
- self.freeRotate = rot
-end
-
-function Knob:getCurrentClick()
- return math.round(self.rotation/self.clicks,0)
-end
-
-function Knob:getValue()
- return self.rotation
-end
-
-function Knob:setValue(value)
- if value >= 360 then
- self.rotation = value - 360
- else
- self.rotation = value
- end
-end
-
-function Knob:setClicks(click)
- self.rotation = click*self.clicks
-end
-
-function Knob:adjust(amount, backwards)
- self.rotation = (((self.rotation+(amount)*backwards) % 360) + 360) % 360
-end
-
-function Knob:click(amount, backwards)
- if self.freeRotate == nil then
- self.rotation = math.normalize(self.rotation+((amount*self.clicks)*backwards),0,360-self.clicks)
- else
- self.rotation = ((self.rotation+((amount*self.clicks)*backwards) % 360) + 360) % 360
- end
-end
-
-function Knob:draw(selected)
- if selected ~= nil and selected == true then
- gfx.drawRoundRect(self.x-12,self.y-12,24,24,2)
- end
- knob:drawRotated(self.x,self.y,self.rotation)
- gfx.drawCircleAtPoint(self.x,self.y,10)
-end
-
-
-
-function Button:init(x,y,w,h,text,smallfont)
- self.drawSmall = smallfont
- self.x = x
- self.y = y
- self.w = w
- self.h = h
- self.text = text
-end
-
-function Button:draw(selected, pressed)
- local w = self.w
- local h = self.h
-
- if w == nil and h == nil then
- if self.drawSmall == true then
- w = fnt8x8:getTextWidth(self.text)+5
- else
- w = fnt:getTextWidth(self.text)+5
- end
- h = 13
- end
-
- if pressed == true then
- gfx.setColor(gfx.kColorWhite)
- end
-
- local fontdraw = fnt
- gfx.drawRect(self.x,self.y,w,h)
-
- if pressed == true then
- gfx.setColor(gfx.kColorBlack)
- end
-
- if selected ~= nil then
- gfx.drawRoundRect(self.x-3,self.y-3,w+6,h+6,2)
- end
-
- if self.drawSmall ~= nil then
- fontdraw = fnt8x8
- end
- fontdraw:drawText(self.text,self.x+2,self.y+2)
-end
-
-
--- alternate pd.updates
-
-filePicker = {cranked = function() end}
-filePicker.selectedFile = nil
-filePicker.callback = nil
-filePicker.oldUpdate = nil
-filePicker.mode = nil
-filePicker.sample = snd.sampleplayer.new(snd.sample.new(1))
-filePicker.keyTimer = nil
-filePicker.animator = nil
-
-local dirs = {}
-local currentPath = "/"
-local row = filePickList:getSelectedRow()
-dirLocations = {}
-
-function filePicker.open(callback, mode)
- row = filePickList:getSelectedRow()
-
- if callback ~= nil then
- filePicker.callback = callback
- end
- filePicker.mode = mode
-
- dirs = {}
- if mode == "song" then
- currentPath = "/songs/"
- table.insert(dirs,"/")
- table.insert(dirLocations, 2)
- else
- currentPath = "/"
- end
-
- filePicker.anim()
-
- pd.inputHandlers.push(filePicker,true)
- filePicker.oldUpdate = pd.update
- pd.update = filePicker.update
-
- filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
- filePickList:setSelectedRow(1)
- filePickList:scrollToRow(1)
-end
-
-function filePicker.update()
- local ftype = "sample"
- row = filePickList:getSelectedRow()
-
- if filePicker.mode == "song" then
- ftype = "song"
- end
-
- if filePickList.needsDisplay == true then
- filePickList:drawInRect(filePicker.animator:currentValue(),0,400,240)
- fnt8x8:drawTextAligned("choose a "..ftype,200,0,align.center)
- end
-
- pd.timer.updateTimers()
-end
-
-function filePicker.close()
- pd.inputHandlers.pop()
- pd.update = filePicker.oldUpdate
- filePicker.callback(filePicker.selectedFile)
- filePicker.animator = nil
-end
-
-function filePicker.AButtonDown()
- if pd.file.isdir(currentPath..filePickListContents[row]) then
- filePicker.anim()
- table.insert(dirs, currentPath)
- table.insert(dirLocations, row)
- currentPath = currentPath..filePickListContents[row]
- filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
- filePickList:setSelectedRow(1)
- filePickList:scrollToRow(1)
- elseif filePickListContents[row] == "*Ā*" then
- filePicker.anim(true)
- currentPath = table.remove(dirs)
- filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
- local rmed = table.remove(dirLocations)
- filePickList:setSelectedRow(rmed)
- filePickList:scrollToRow(rmed)
- elseif filePickListContents[row] == "*Ą* record sample" then
- sampleScreen.open(function(sample)
- filePicker.selectedFile = sample
- filePicker.close()
- end)
- elseif filePickListContents[row] == "*ċ* edit sample" then
- sampleEditScreen.open(snd.sample.new("temp/"..listview:getSelectedRow()..".pda"), function(sample)
- filePicker.selectedFile = sample
- filePicker.close()
- end, pd.datastore.readImage("temp/"..listview:getSelectedRow()..".pdi"))
- else
- if filePicker.mode ~= "song" or string.find(filePickListContents[row],"%/ %(song%)") then
- filePicker.selectedFile = string.unnormalize(currentPath..filePickListContents[row])
- filePicker.close()
- end
- end
-end
-
-function filePicker.BButtonDown()
- if currentPath == "/" then
- filePicker.selectedFile = "none"
- filePicker.close()
- else
- currentPath = table.remove(dirs)
- filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
- local rmed = table.remove(dirLocations)
- filePickList:setSelectedRow(rmed)
- filePickList:scrollToRow(rmed)
- filePicker.anim(true)
- end
-end
-
-function filePicker.rightButtonDown()
- if not pd.file.isdir(currentPath..filePickListContents[row]) and string.find(filePickListContents[row],"%.pda") then
- filePicker.sample:stop()
- filePicker.sample:setSample(snd.sample.new(string.unnormalize(currentPath..filePickListContents[row])))
- filePicker.sample:play()
- end
-end
-
-function filePicker.upButtonDown()
- local function callback()
- filePickList:selectPreviousRow()
- end
- filePicker.keyTimer = pd.timer.keyRepeatTimerWithDelay(300,75,callback)
-end
-
-function filePicker.downButtonDown()
- local function callback()
- filePickList:selectNextRow()
- end
- filePicker.keyTimer = pd.timer.keyRepeatTimerWithDelay(300,75,callback)
-end
-
-function filePicker.upButtonUp()
- if filePicker.keyTimer ~= nil then
- filePicker.keyTimer:remove()
- end
-end
-
-function filePicker.downButtonUp()
- if filePicker.keyTimer ~= nil then
- filePicker.keyTimer:remove()
- end
-end
-
-function filePicker.anim(back)
- gfx.clear()
- if back then
- filePicker.animator = gfx.animator.new(200, -200, 0, pd.easingFunctions.outQuart)
- else
- filePicker.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
- end
-end
-
-function filePicker.modifyDirContents(val)
- if currentPath == "/" then
- for i=#val, 1, -1 do
- if val[i] ~= "samples/" and val[i] ~= "temp/" and val[i] ~= "songs/" then
- table.remove(val, i)
- end
- end
- end
-
- for i=1, #val do
- if pd.file.isdir(currentPath..val[i]) then
- if filePicker.mode == "song" and table.find(pd.file.listFiles(currentPath..val[i]),"song.json") ~= -1 then
- val[i] = val[i].." (song)"
- end
- end
- val[i] = string.normalize(val[i])
- end
-
- if currentPath ~= "/" then
- table.insert(val,1,"*Ā*")
- elseif filePicker.mode ~= "song" then
- table.insert(val,1,"*Ą* record sample")
- end
-
- if filePicker.mode == "newsmp" and currentPath == "/" then
- table.insert(val,1,"*ċ* edit sample")
- end
-
- return val
-end
-
-sampleScreen = {}
-sampleScreen.sample = nil
-sampleScreen.callback = nil
-sampleScreen.recording = false
-sampleScreen.oldUpdate = nil
-sampleScreen.waiting = false
-sampleScreen.waitForButton = false
-sampleScreen.recAt = 0.15
-sampleScreen.recTimer = pd.timer.new(5000)
-sampleScreen.waveformImage = nil
-sampleScreen.waveformAnimator = nil
-sampleScreen.waveformLastXY = {0,20}
-
-local state = "press A to arm..."
-
-function sampleScreen.open(callback)
- sampleScreen.sample = nil
- sampleScreen.callback = nil
- sampleScreen.recording = false
- sampleScreen.oldUpdate = nil
- sampleScreen.waiting = false
- sampleScreen.waitForButton = false
- sampleScreen.recTimer = pd.timer.new(5000)
- sampleScreen.recTimer:reset()
- sampleScreen.recTimer:pause()
- sampleScreen.waveformImage = gfx.image.new(400, 45)
- sampleScreen.waveformLastXY = {0,20}
- state = "press A to arm..."
-
-
- if callback ~= nil then
- sampleScreen.callback = callback
- end
-
- if settings["stoponsample"] == true then
- seq:stop()
- seq:allNotesOff()
- end
-
- pd.inputHandlers.push(sampleScreen,true)
- sampleScreen.oldUpdate = pd.update
- pd.update = sampleScreen.update
- snd.micinput.startListening()
-end
-
-function sampleScreen.update()
- gfx.clear()
- if sampleScreen.waiting == true and snd.micinput.getLevel() > sampleScreen.recAt then
- state = "recording..."
- sampleScreen.waveformAnimator = gfx.animator.new(5000, 1, 400)
- sampleScreen.record()
- end
-
- if sampleScreen.recording == true then
- local lastxy = sampleScreen.waveformLastXY
- local x = sampleScreen.waveformAnimator:currentValue()
- local y = 40+((-snd.micinput.getLevel())*40)
- gfx.pushContext(sampleScreen.waveformImage)
- gfx.drawLine(lastxy[1], lastxy[2], x, y)
- gfx.popContext()
- sampleScreen.waveformLastXY = {x, y}
- sampleScreen.waveformImage:draw(0, 55)
- end
-
- gfx.drawTextAligned(state,200,0,align.center)
- gfx.drawRect(50,110,300,20)
- gfx.fillRect(50,110,snd.micinput.getLevel()*300,20)
- fnt8x8:drawTextAligned(math.round(snd.micinput.getLevel(),2),200,116,align.center)
- fnt8x8:drawTextAligned("will start recording from "..snd.micinput.getSource().." if volume = "..sampleScreen.recAt,200,231,align.center)
- gfx.drawTextAligned(tostring(sampleScreen.recTimer.currentTime/1000).." / 5.0",200,20,align.center)
- pd.timer.updateTimers()
-end
-
-function sampleScreen.close()
- pd.inputHandlers.pop()
- pd.update = sampleScreen.oldUpdate
- if settings["saveWaveforms"] == true then
- sampleScreen.callback({sampleScreen.sample, sampleScreen.waveformImage:copy()})
- else
- sampleScreen.callback(sampleScreen.sample)
- end
-
- if settings["stoponsample"] == true then
- seq:play()
- end
-end
-
-function sampleScreen.record()
- sampleScreen.recording = true
- sampleScreen.waiting = false
- sampleScreen.recTimer:reset()
- sampleScreen.recTimer:start()
-
- local format = snd.kFormat16bitMono
-
- if settings["sample16bit"] == false then
- format = snd.kFormat8bitMono
- end
-
- local buffer = snd.sample.new(5, format)
- snd.micinput.recordToSample(buffer, function(smp)
- sampleScreen.sample = smp
- snd.micinput.stopListening()
- if sampleScreen.sample == "none" then
- goto continue
- end
-
- sampleScreen.recording = false
- sampleScreen.waitForButton = true
-
- gfx.clear()
- smp:play()
- gfx.drawTextInRect("save?\n\na to save, b to redo, right to hear again",20,85,360,200,nil,nil,align.center)
- sampleScreen.waveformImage:drawCentered(400-(sampleScreen.waveformAnimator:currentValue()/2), 45)
- pd.stop()
- ::continue::
- end)
-end
-
-function sampleScreen.AButtonDown()
- if sampleScreen.waitForButton == true then
- pd.start()
- displayInfo("saved as "..listview:getSelectedRow()..".pda")
- sampleScreen.sample:save(songdir..listview:getSelectedRow()..".pda")
- if settings["savewavs"] == true then
- sampleScreen.sample:save(songdir..listview:getSelectedRow()..".wav")
- end
- sampleScreen.close()
- elseif sampleScreen.waiting == false and sampleScreen.recording == false then
- sampleScreen.waiting = true
- state = "armed, waiting..."
- else
- snd.micinput.stopRecording()
- end
-end
-
-function sampleScreen.BButtonDown()
- if sampleScreen.waitForButton == true then
- pd.start()
- sampleScreen.sample = nil
- sampleScreen.recording = false
- sampleScreen.oldUpdate = nil
- sampleScreen.waiting = false
- sampleScreen.waitForButton = false
- sampleScreen.recTimer:reset()
- sampleScreen.recTimer:pause()
- sampleScreen.waveformImage = gfx.image.new(400, 45)
- sampleScreen.waveformAnimator = nil
- sampleScreen.waveformLastXY = {0,20}
-
- state = "press A to arm..."
-
- snd.micinput.startListening()
- elseif sampleScreen.recording == false then
- sampleScreen.sample = "none"
- snd.micinput.stopListening()
- sampleScreen.close()
- end
-end
-
-function sampleScreen.upButtonDown()
- sampleScreen.recAt += 0.05
- sampleScreen.fixRec()
-end
-
-function sampleScreen.rightButtonDown()
- if sampleScreen.waitForButton == true then
- sampleScreen.sample:play()
- else
- sampleScreen.recAt += 0.01
- sampleScreen.fixRec()
- end
-end
-
-function sampleScreen.downButtonDown()
- sampleScreen.recAt -= 0.05
- sampleScreen.fixRec()
-end
-
-function sampleScreen.leftButtonDown()
- sampleScreen.recAt -= 0.01
- sampleScreen.fixRec()
-end
-
-function sampleScreen.fixRec()
- sampleScreen.recAt = math.round(sampleScreen.recAt,2)
- sampleScreen.recAt = math.max(0.0, math.min(1.0, (sampleScreen.recAt)))
-end
-
-keyboardScreen = {}
-keyboardScreen.oldUpdate = nil
-keyboardScreen.callback = nil
-keyboardScreen.askingForOK = false
-keyboardScreen.prompt = ""
-keyboardScreen.text = ""
-keyboardScreen.limit = nil
-keyboardScreen.origtext = ""
-
-function keyboardScreen.open(prompt,text,limit,callback)
- keyboardScreen.callback = callback
- keyboardScreen.text = string.normalize(text)
- keyboardScreen.origtext = text
- keyboardScreen.prompt = prompt
- keyboardScreen.askingForOK = false
-
- if limit == nil then
- limit = 100000
- end
- keyboardScreen.limit = limit
-
-
- pd.inputHandlers.push(keyboardScreen,true)
- keyboardScreen.oldUpdate = pd.update
- pd.update = keyboardScreen.update
-
- pd.keyboard.show(text)
-end
-
-function keyboardScreen.update()
- local rectWidth = 400-pd.keyboard.width()
- gfx.clear()
-
- if keyboardScreen.askingForOK == false then
- gfx.drawTextInRect(keyboardScreen.prompt,0,0,rectWidth,100,nil,nil,align.center)
- gfx.drawTextInRect(keyboardScreen.text,0,104,rectWidth,136,nil,nil,align.center)
- else
- gfx.drawTextInRect("is this good?\n\na to confirm, b to redo, left to quit",0,0,rectWidth,100,nil,nil,align.center)
- gfx.drawTextInRect(keyboardScreen.text,0,104,rectWidth,136,nil,nil,align.center)
- end
-end
-
-function keyboardScreen.close()
- pd.inputHandlers.pop()
- keyboardScreen.callback(keyboardScreen.text)
- pd.update = keyboardScreen.oldUpdate
-end
-
-function pd.keyboard.keyboardWillHideCallback()
- keyboardScreen.askingForOK = true
-end
-
-function pd.keyboard.textChangedCallback()
- if #pd.keyboard.text <= keyboardScreen.limit then
- keyboardScreen.text = string.normalize(pd.keyboard.text)
- else
- pd.keyboard.text = string.sub(pd.keyboard.text,1,keyboardScreen.limit)
- end
-end
-
-function keyboardScreen.AButtonDown()
- if keyboardScreen.askingForOK == true then
- keyboardScreen.close()
- end
-end
-
-function keyboardScreen.BButtonDown()
- if keyboardScreen.askingForOK == true then
- keyboardScreen.askingForOK = false
- pd.keyboard.show(keyboardScreen.text)
- end
-end
-
-function keyboardScreen.leftButtonDown()
- keyboardScreen.text = "_EXITED_KEYBOĀRD"
- keyboardScreen.close()
-end
-
-
-settingsScreen = {}
-settingsScreen.subMenu = ""
-settingsScreen.oldUpdate = nil
-settingsScreen.animator = nil
-settingsScreen.updateOutputs = (function()
- if settings["output"] < 3 then
- settings["output"] += 1
- else
- settings["output"] = 0
- snd.setOutputsActive(false, true)
- end
- if settings["output"] == 1 then
- snd.setOutputsActive(true, false)
- elseif settings["output"] == 2 then
- snd.setOutputsActive(true, true)
- elseif settings["output"] == 3 then
- local state = snd.getHeadphoneState()
- snd.setOutputsActive(state, not state)
- end
-end)
-
-function settingsScreen.open()
- settingsScreen.updateSettings()
-
- pd.getSystemMenu():removeAllMenuItems()
-
- settingsList:setSelectedRow(1)
- settingsList:scrollToRow(1)
-
- settingsScreen.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
-
- pd.inputHandlers.push(settingsScreen,true)
- settingsScreen.oldUpdate = pd.update
- pd.update = settingsScreen.update
-end
-
-function settingsScreen.update()
- if settingsList.needsDisplay or settingsScreen.animator:ended() == false
- then
- gfx.clear()
- settingsList:drawInRect(settingsScreen.animator:currentValue(),0,400,240)
-
- if settingsScreen.subMenu == "" then
- fnt8x8:drawTextAligned("settings",200,0,align.center)
- else
- fnt8x8:drawTextAligned("settings/"..string.sub(settingsScreen.subMenu,1,#settingsScreen.subMenu-1),200,0,align.center)
- end
- fnt8x8:drawTextAligned("cs-16 version "..pd.metadata.version..", build "..pd.metadata.buildNumber,200,231,align.center)
- end
-
- pd.timer.updateTimers()
-end
-
-function settingsScreen.close()
- settingsScreen.animator = nil
- pd.inputHandlers.pop()
- pd.update = settingsScreen.oldUpdate
-
- applyMenuItems("song")
-end
-
-function settingsScreen.downButtonDown()
- settingsList:selectNextRow()
-end
-
-function settingsScreen.upButtonDown()
- settingsList:selectPreviousRow()
-end
-
-function settingsScreen.leftButtonDown()
- if settingsList:getSelectedRow() == 3 and settingsScreen.subMenu == "general/" then
- settingsScreen.updateOutputs()
- elseif settingsList:getSelectedRow() == 4 and settingsScreen.subMenu == "general/" then
- settings["cranksens"] = table.cycle(crankSensList, settings["cranksens"],true)
- end
- settingsScreen.updateSettings()
-end
-
-function settingsScreen.rightButtonDown()
- if settingsList:getSelectedRow() == 3 and settingsScreen.subMenu == "general/" then
- settingsScreen.updateOutputs()
- elseif settingsList:getSelectedRow() == 4 and settingsScreen.subMenu == "general/" then
- settings["cranksens"] = table.cycle(crankSensList, settings["cranksens"])
- end
- settingsScreen.updateSettings()
-end
-
-function settingsScreen.BButtonDown()
- if settingsScreen.subMenu == "" then
- settingsScreen.close()
- else
- local oldmenu = settingsScreen.subMenu
- settingsScreen.subMenu = ""
- settingsScreen.updateSettings()
- settingsList:setSelectedRow(table.indexOfElement(settingsList:get(), oldmenu))
- settingsScreen.animator = gfx.animator.new(200, -200, 0, pd.easingFunctions.outQuart)
- end
-end
-
-function settingsScreen.AButtonDown()
- local row = settingsList:getSelectedRow()
- local text = settingsListContents[settingsList:getSelectedRow()]
-
- local refresh = true
-
- if settingsScreen.subMenu == "" then
- settingsScreen.subMenu = text
- settingsList:setSelectedRow(1)
- settingsScreen.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
- else
- if text == "*Ā*" then
- local oldmenu = settingsScreen.subMenu
- settingsScreen.subMenu = ""
- settingsScreen.updateSettings()
- settingsList:setSelectedRow(table.indexOfElement(settingsList:get(), oldmenu))
- settingsScreen.animator = gfx.animator.new(200, -200, 0, pd.easingFunctions.outQuart)
- refresh = false
- end
-
- if settingsScreen.subMenu == "ui/" then
- if row == 2 then
- settings["dark"] = not settings["dark"]
- pd.display.setInverted(settings["dark"])
- elseif row == 3 then
- if settings["visualizer"] < 3 then
- settings["visualizer"] += 1
- else
- settings["visualizer"] = 0
- end
- elseif row == 4 then
- settings["num/max"] = not settings["num/max"]
- elseif row == 5 then
- settings["showNoteNames"] = not settings["showNoteNames"]
- elseif row == 6 then
- settings["screenAnimation"] = not settings["screenAnimation"]
- elseif row == 7 then
- settings["useSystemFont"] = not settings["useSystemFont"]
- if settings["useSystemFont"] == true then
- fnt = gfx.getSystemFont()
- else
- fnt = gfx.font.new("fnt/modified-tron")
- end
- gfx.setFont(fnt)
- elseif row == 8 then
- if settings["pmode"] == false then
- messageBox.open("\n\nwarning!\n\nrunning cs-16 at 50fps will reduce your battery life, but improve performance.\n\nare you sure you want to enable this?\n\na = yes, b = no", function(ans)
- if ans == "yes" then
- settings["pmode"] = not settings["pmode"]
- if settings["pmode"] == true then
- pd.display.setRefreshRate(50)
- else
- pd.display.setRefreshRate(30)
- end
- end
- settingsScreen.updateSettings()
- end)
- else
- settings["pmode"] = not settings["pmode"]
- pd.display.setRefreshRate(30)
- end
- end
- elseif settingsScreen.subMenu == "behavior/" then
- if row == 2 then
- settings["playonload"] = not settings["playonload"]
- elseif row == 3 then
- settings["stoponsample"] = not settings["stoponsample"]
- elseif row == 4 then
- settings["stopontempo"] = not settings["stopontempo"]
- elseif row == 5 then
- settings["savewavs"] = not settings["savewavs"]
- end
- elseif settingsScreen.subMenu == "recording/" then
- if row == 8 then
- local quant = settings["recordQuantization"]
- if quant == 1 then -- every #th note
- settings["recordQuantization"] = 2
- elseif quant == 2 then
- settings["recordQuantization"] = 4
- elseif quant == 4 then
- settings["recordQuantization"] = 1
- end
- elseif row > 1 then
- keyboardScreen.open("enter a new track number for this button (1-16):","",2,function(t)
- local num = tonumber(t)
- if num ~= nil then
- if num > 0 and num < 17 then
- if row == 2 then
- settings["aRecTrack"] = num
- elseif row == 3 then
- settings["bRecTrack"] = num
- elseif row == 4 then
- settings["upRecTrack"] = num
- elseif row == 5 then
- settings["downRecTrack"] = num
- elseif row == 6 then
- settings["leftRecTrack"] = num
- elseif row == 7 then
- settings["rightRecTrack"] = num
- end
- settingsScreen.updateSettings()
- end
- end
- end)
- end
- elseif settingsScreen.subMenu == "general/" then
- if row == 2 then
- keyboardScreen.open("enter new author name:",settings["author"],15,function(t)
- if t ~= "_EXITED_KEYBOĀRD" then
- settings["author"] = t
- if songdir == "temp/" then
- songAuthor = t
- end
- settingsScreen.updateSettings()
- end
- end)
- elseif row == 3 then
- settingsScreen.updateOutputs()
- elseif row == 4 then
- settings["cranksens"] = table.cycle(crankSensList, settings["cranksens"])
- elseif row == 5 then
- creditsScreen.open()
- end
- elseif settingsScreen.subMenu == "sampling/" then
- if row == 2 then
- settings["sample16bit"] = not settings["sample16bit"]
- elseif row == 3 then
- settings["saveWaveforms"] = not settings["saveWaveforms"]
- end
- end
- end
-
- if refresh then
- settingsScreen.updateSettings()
- end
-end
-
-function settingsScreen.updateSettings()
- if settingsScreen.subMenu == "" then
- settingsList:set({"general/","behavior/","recording/","sampling/","ui/"})
- elseif settingsScreen.subMenu == "general/" then
- local outputText = "speaker"
- if settings["output"] == 1 then
- outputText = "headset"
- elseif settings["output"] == 2 then
- outputText = "speaker, headset"
- elseif settings["output"] == 3 then
- outputText = "auto"
- end
-
- settingsList:set({
- "*Ā*",
- "author: "..settings["author"],
- "output: "..outputText,
- "crank speed: "..settings["cranksens"],
- "credits..."
- })
- elseif settingsScreen.subMenu == "behavior/" then
- settingsList:set({
- "*Ā*",
- "play on load: "..tostring(settings["playonload"]),
- "stop if sampling: "..tostring(settings["stoponsample"]),
- "tempo edit stop: "..tostring(settings["stopontempo"]),
- "save .wav samples: "..tostring(settings["savewavs"])
- })
- elseif settingsScreen.subMenu == "recording/" then
- settingsList:set({
- "*Ā*",
- "_Ⓐ_ button track: "..tostring(settings["aRecTrack"]),
- "_Ⓑ_ button track: "..tostring(settings["bRecTrack"]),
- "_⬆️_ button track: "..tostring(settings["upRecTrack"]),
- "_⬇️_ button track: "..tostring(settings["downRecTrack"]),
- "_⬅️_ button track: "..tostring(settings["leftRecTrack"]),
- "_➡️_ button track: "..tostring(settings["rightRecTrack"]),
- "quantization: "..tostring(settings["recordQuantization"])
- })
- elseif settingsScreen.subMenu == "sampling/" then
- local format = "16 bit"
- if settings["sample16bit"] == false then
- format = "8 bit"
- end
- settingsList:set({
- "*Ā*",
- "sample format: "..format,
- "save waveforms: "..tostring(settings["saveWaveforms"])
- })
- elseif settingsScreen.subMenu == "ui/" then
- local vistext = "both"
-
- if settings["visualizer"] == 0 then
- vistext = "none"
- elseif settings["visualizer"] == 1 then
- vistext = "notes"
- elseif settings["visualizer"] == 2 then
- vistext = "sine"
- end
-
- settingsList:set({
- "*Ā*",
- "dark mode: "..tostring(settings["dark"]),
- "visualizer: "..vistext,
- "show number/total: "..tostring(settings["num/max"]),
- "show note names: "..tostring(settings["showNoteNames"]),
- "animate scrn move: "..tostring(settings["screenAnimation"]),
- "use system font: "..tostring(settings["useSystemFont"]),
- "50fps: "..tostring(settings["pmode"])
- })
- end
- saveSettings()
-end
-
-
-sampleEditScreen = {}
-sampleEditScreen.oldUpdate = nil
-sampleEditScreen.sample = nil
-sampleEditScreen.editedSample = nil
-sampleEditScreen.callback = nil
-sampleEditScreen.changeVal = 1000
-sampleEditScreen.sampleLen = 0
-sampleEditScreen.trim = {0,0}
-sampleEditScreen.side = 1 -- 1 = begin, 2 = end
-sampleEditScreen.ctrPixel = 0
-
-function sampleEditScreen.open(sample, callback, image)
- sampleEditScreen.sample = sample
- sampleEditScreen.sampleImg = image
- sampleEditScreen.editedSample = nil
- sampleEditScreen.callback = callback
- sampleEditScreen.changeVal = 1000
- sampleEditScreen.trim = {0,0}
- sampleEditScreen.side = 1
-
- if image ~= nil then
- local done = false
- for x = 400, 0, -1 do
- for y = 40, 0, -1 do
- if image:sample(x, y) == gfx.kColorBlack then
- sampleEditScreen.ctrPixel = 400-(x/2)
- done = true
- break
- end
- end
- if done then
- break
- end
- end
- end
-
- sampleEditScreen.sampleLen = math.round(sample:getLength()*44100,0)
-
- sampleEditScreen.trim[2] = sampleEditScreen.sampleLen
-
- sampleEditScreen.editedSample = sampleEditScreen.sample:getSubsample(sampleEditScreen.trim[1],sampleEditScreen.trim[2])
- sampleEditScreen.samplePlayer = snd.sampleplayer.new(sampleEditScreen.editedSample)
-
- pd.inputHandlers.push(sampleEditScreen,true)
- sampleEditScreen.oldUpdate = pd.update
- pd.update = sampleEditScreen.update
-
- gfx.clear()
-
- sample:play()
-end
-
-function sampleEditScreen.update() -- TODO: stop animation timer when the sample has finished playing, and figure out where the line should start from trim
- local sidetext = "start"
- gfx.clear()
- local crank = pd.getCrankTicks(settings["cranksens"])
- local side = sampleEditScreen.side
-
- if crank ~= 0 then
- local otherside = 1 -- IS THAT A MINECRAFT REFERENCE????? :OOOO
- sampleEditScreen.trim[side] += sampleEditScreen.changeVal*crank
- sampleEditScreen.trim[side] = math.normalize(sampleEditScreen.trim[side],0,sampleEditScreen.sampleLen)
-
- if side == 1 then
- otherside = 2
- if sampleEditScreen.trim[side] >= sampleEditScreen.trim[otherside] then
- sampleEditScreen.trim[side] = sampleEditScreen.trim[otherside] - 10
- end
- else
- if sampleEditScreen.trim[side] <= sampleEditScreen.trim[otherside] then
- sampleEditScreen.trim[side] = sampleEditScreen.trim[otherside] + 10
- end
- end
- sampleEditScreen.editedSample = sampleEditScreen.sample:getSubsample(sampleEditScreen.trim[1],sampleEditScreen.trim[2])
- sampleEditScreen.samplePlayer:setSample(sampleEditScreen.editedSample)
- sampleEditScreen.samplePlayer:play()
- end
-
- if sampleEditScreen.side == 2 then
- sidetext = "end"
- end
-
- gfx.drawTextAligned("start: "..sampleEditScreen.trim[1]..", end: "..sampleEditScreen.trim[2],200,104,align.center)
- gfx.drawTextAligned("selected: "..sidetext,200,120,align.center)
- gfx.drawTextAligned("a to save, b to discard",200,210,align.center)
- fnt8x8:drawTextAligned("changing frames by "..sampleEditScreen.changeVal,200,231,align.center)
-
- if sampleEditScreen.sampleImg ~= nil then
- sampleEditScreen.sampleImg:drawCentered(sampleEditScreen.ctrPixel, 40)
- end
-end
-
-function sampleEditScreen.rightButtonDown()
- sampleEditScreen.side = 2
-end
-
-function sampleEditScreen.leftButtonDown()
- sampleEditScreen.side = 1
-end
-
-function sampleEditScreen.upButtonDown()
- sampleEditScreen.changeVal = math.normalize(sampleEditScreen.changeVal+50,50,2000)
-end
-
-function sampleEditScreen.downButtonDown()
- sampleEditScreen.changeVal = math.normalize(sampleEditScreen.changeVal-50,50,2000)
-end
-
-function sampleEditScreen.close(sample)
- pd.inputHandlers.pop()
- pd.update = sampleEditScreen.oldUpdate
- sampleEditScreen.callback(sample)
-end
-
-function sampleEditScreen.BButtonDown()
- sampleEditScreen.close(sampleEditScreen.sample)
-end
-
-function sampleEditScreen.AButtonDown()
- sampleEditScreen.close(sampleEditScreen.editedSample)
-end
-
-messageBox = {}
-messageBox.oldUpdate = nil
-messageBox.callback = nil
---messageBox.message = ""
-
-function messageBox.open(message, callback)
- gfx.clear()
- messageBox.callback = callback
- gfx.drawTextInRect(message,0,0,400,240,nil,nil,align.center)
-
- pd.inputHandlers.push(messageBox,true)
- messageBox.oldUpdate = pd.update
- pd.update = messageBox.update
-end
-
-function messageBox.update()
-
-end
-
-function messageBox.AButtonDown()
- messageBox.close("yes")
-end
-
-function messageBox.BButtonDown()
- messageBox.close("no")
-end
-
-function messageBox.close(ans)
- pd.inputHandlers.pop()
- pd.update = messageBox.oldUpdate
- if messageBox.callback ~= nil then
- messageBox.callback(ans)
- end
-end
-
-creditsScreen = {}
-creditsScreen.oldUpdate = nil
-creditsScreen.current = 1
-
-function creditsScreen.open()
- creditsScreen.current = 1
- creditsScreen.updateText()
-
- pd.inputHandlers.push(creditsScreen,true)
- creditsScreen.oldUpdate = pd.update
- pd.update = creditsScreen.update
-end
-
-function creditsScreen.update()
-
-end
-
-function creditsScreen.updateText()
- local text
- gfx.clear()
-
- if creditsScreen.current == 1 then -- used if statements so credits could be dynamic!
- text = "\ncs-16 v"..pd.metadata.version.."\n\ndeveloped by nanobot567\n\n\n\n-- feature requesters --"
- elseif creditsScreen.current == 2 then
- text = "\nspecial thanks to...\n\n\n\n\n\n\n\n\n\n\nyou guys are awesome! :)"
- end
-
- gfx.drawTextInRect(text,0,0,400,240,nil,nil,align.center)
-
- if creditsScreen.current == 1 then
- gfx.drawTextInRect("\n\ndrhitchcockco - number/total setting\n\njustyouraveragehomie - waveform view, live record, and much more!", 0, 128, 400, 240, nil, nil, align.center, fnt8x8)
- elseif creditsScreen.current == 2 then
- gfx.drawTextInRect("my family\nlilfigurative\ntrisagion media\nthe trisagion insurgence", 0, 48, 400, 240, nil, nil, align.center, fnt8x8)
- elseif creditsScreen.current == 3 then
- gfx.drawTextInRect("nanobot567: open source, forever.\n\n\n\nthe cs-16 font is a modified version of the 'Tron' font from idleberg's playdate arcade fonts (https://github.com/idleberg/playdate-arcade-fonts).\n\nall of the source code is under the mit license, and is available at https://is.gd/cs16m (capital letters) (https://github.com/nanobot567/cs-16).", 2, 0, 396, 240, nil, nil, align.center, fnt8x8)
- end
- gfx.drawTextInRect("credits - use left / right to navigate, B to exit", 0, 232, 400, 8, nil, nil, align.center, fnt8x8)
-end
-
-function creditsScreen.BButtonDown()
- creditsScreen.close()
-end
-
-function creditsScreen.leftButtonDown()
- if creditsScreen.current > 1 then
- creditsScreen.current -= 1
- creditsScreen.updateText()
- end
-end
-
-function creditsScreen.rightButtonDown()
- if creditsScreen.current < 3 then
- creditsScreen.current += 1
- creditsScreen.updateText()
- else
- creditsScreen.close()
- end
-end
-
-function creditsScreen.close()
- pd.inputHandlers.pop()
- pd.update = creditsScreen.oldUpdate
-end
+import "ui/button"
+import "ui/knob"
+import "ui/message"
+import "ui/screens/credits"
+import "ui/screens/filepicker"
+import "ui/screens/keyboard"
+import "ui/screens/log"
+import "ui/screens/sample"
+import "ui/screens/sampleedit"
+import "ui/screens/settings"
diff --git a/src/ui/button.lua b/src/ui/button.lua
new file mode 100644
index 0000000..a84b89b
--- /dev/null
+++ b/src/ui/button.lua
@@ -0,0 +1,47 @@
+-- button ui class
+
+class("Button").extends()
+
+function Button:init(x, y, w, h, text, smallfont)
+ self.drawSmall = smallfont
+ self.x = x
+ self.y = y
+ self.w = w
+ self.h = h
+ self.text = text
+end
+
+function Button:draw(selected, pressed)
+ local w = self.w
+ local h = self.h
+
+ if w == nil and h == nil then
+ if self.drawSmall == true then
+ w = fnt8x8:getTextWidth(self.text) + 5
+ else
+ w = fnt:getTextWidth(self.text) + 5
+ end
+ h = 13
+ end
+
+ if pressed == true then
+ gfx.setColor(gfx.kColorWhite)
+ end
+
+ local fontdraw = fnt
+ gfx.drawRect(self.x, self.y, w, h)
+
+ if pressed == true then
+ gfx.setColor(gfx.kColorBlack)
+ end
+
+ if selected ~= nil then
+ gfx.drawRoundRect(self.x - 3, self.y - 3, w + 6, h + 6, 2)
+ end
+
+ if self.drawSmall ~= nil then
+ fontdraw = fnt8x8
+ end
+ fontdraw:drawText(self.text, self.x + 2, self.y + 2)
+end
+
diff --git a/src/ui/knob.lua b/src/ui/knob.lua
new file mode 100644
index 0000000..3b7e243
--- /dev/null
+++ b/src/ui/knob.lua
@@ -0,0 +1,53 @@
+-- knob ui class
+
+class("Knob").extends()
+
+function Knob:init(x, y, clicks, rot)
+ self.x = x
+ self.y = y
+ self.clicks = 360 / clicks
+ self.rotation = 0
+ self.freeRotate = rot
+end
+
+function Knob:getCurrentClick()
+ return math.round(self.rotation / self.clicks, 0)
+end
+
+function Knob:getValue()
+ return self.rotation
+end
+
+function Knob:setValue(value)
+ if value >= 360 then
+ self.rotation = value - 360
+ else
+ self.rotation = value
+ end
+end
+
+function Knob:setClicks(click)
+ self.rotation = click * self.clicks
+end
+
+function Knob:adjust(amount, backwards)
+ self.rotation = (((self.rotation + (amount) * backwards) % 360) + 360) % 360
+end
+
+function Knob:click(amount, backwards)
+ if self.freeRotate == nil then
+ self.rotation = math.normalize(self.rotation + ((amount * self.clicks) * backwards), 0, 360 - self.clicks)
+ else
+ self.rotation = ((self.rotation + ((amount * self.clicks) * backwards) % 360) + 360) % 360
+ end
+end
+
+function Knob:draw(selected)
+ if selected ~= nil and selected == true then
+ gfx.drawRoundRect(self.x - 12, self.y - 12, 24, 24, 2)
+ end
+ knob:drawRotated(self.x, self.y, self.rotation)
+ gfx.drawCircleAtPoint(self.x, self.y, 10)
+end
+
+
diff --git a/src/ui/message.lua b/src/ui/message.lua
new file mode 100644
index 0000000..a2d1d65
--- /dev/null
+++ b/src/ui/message.lua
@@ -0,0 +1,40 @@
+-- message box
+
+messageBox = {}
+messageBox.oldUpdate = nil
+messageBox.callback = nil
+--messageBox.message = ""
+
+function messageBox.open(message, callback)
+ gfx.clear()
+ messageBox.callback = callback
+
+ local w, h = gfx.getTextSizeForMaxWidth(message, 400)
+
+ gfx.drawTextInRect(message, 0, 120 - (h / 2), 400, h, nil, nil, align.center)
+
+ pd.inputHandlers.push(messageBox, true)
+ messageBox.oldUpdate = pd.update
+ pd.update = messageBox.update
+end
+
+function messageBox.update()
+
+end
+
+function messageBox.AButtonDown()
+ messageBox.close("yes")
+end
+
+function messageBox.BButtonDown()
+ messageBox.close("no")
+end
+
+function messageBox.close(ans)
+ pd.inputHandlers.pop()
+ pd.update = messageBox.oldUpdate
+ if messageBox.callback ~= nil then
+ messageBox.callback(ans)
+ end
+end
+
diff --git a/src/ui/screens/credits.lua b/src/ui/screens/credits.lua
new file mode 100644
index 0000000..f682a65
--- /dev/null
+++ b/src/ui/screens/credits.lua
@@ -0,0 +1,82 @@
+creditsScreen = {}
+creditsScreen.oldUpdate = nil
+creditsScreen.current = 1
+
+creditsScreen.features = { "drhitchcockco - number/total setting",
+ "justyouraveragehomie - waveform view, live record, swing, and much more!" }
+creditsScreen.thanks = { "my family", "lil figurative", "trisagion media", "the trisagion insurgence",
+ "r.y.e. (i taikatsu'y, y'yaitimu!! mrak'y ni t'dumeri-tae-ou kitsu-kus.)",
+ "m.r. (hei vibao!!)" }
+
+function creditsScreen.open()
+ inScreen = true
+
+ creditsScreen.current = 1
+ creditsScreen.updateText()
+
+ pd.inputHandlers.push(creditsScreen, true)
+ creditsScreen.oldUpdate = pd.update
+ pd.update = creditsScreen.update
+end
+
+function creditsScreen.update()
+
+end
+
+function creditsScreen.updateText()
+ local text
+ gfx.clear()
+
+ if creditsScreen.current == 1 then -- used if statements so credits could be dynamic!
+ text = "\ncs-16 v" .. pd.metadata.version .. "\n\ndeveloped by nanobot567\n\n\n\n-- feature requesters --"
+ elseif creditsScreen.current == 2 then
+ text = "\nspecial thanks to...\n\n\n\n\n\n\n\n\n\n\nyou guys are awesome! :)"
+ end
+
+ gfx.drawTextInRect(text, 0, 0, 400, 240, nil, nil, align.center)
+
+ if creditsScreen.current == 1 then
+ gfx.drawTextInRect(
+ "\n\n" .. table.concat(creditsScreen.features, "\n\n"),
+ 0,
+ 128, 400, 240, nil, nil, align.center, fnt8x8)
+ elseif creditsScreen.current == 2 then
+ gfx.drawTextInRect(
+ table.concat(creditsScreen.thanks, "\n\n"), 0, 48,
+ 400, 240, nil, nil,
+ align.center, fnt8x8)
+ elseif creditsScreen.current == 3 then
+ gfx.drawTextInRect(
+ "nanobot567: open source, forever.\n\n\n\nthe cs-16 font is a modified version of the 'Tron' font from idleberg's playdate arcade fonts (https://github.com/idleberg/playdate-arcade-fonts).\n\nthe 'rains' fonts (referred to as the system fonts) are from the playdate sdk resources folder, with a couple positioning changes.\n\nall of the source code is under the mit license and is available at...\n\nhttps://github.com/nanobot567/cs-16\n\nthe cs-16 manual is available online at...\n\nhttps://(CS-16 GH)/blob/main/MANUAL.md\n\n\n\nthanks for using cs-16!!",
+ 2, 0, 396, 240, nil, nil, align.center, fnt8x8)
+ end
+ gfx.drawTextInRect("credits - use left / right to navigate, B to exit", 0, 232, 400, 8, nil, nil, align.center, fnt8x8)
+end
+
+function creditsScreen.BButtonDown()
+ creditsScreen.close()
+end
+
+function creditsScreen.leftButtonDown()
+ if creditsScreen.current > 1 then
+ creditsScreen.current -= 1
+ creditsScreen.updateText()
+ end
+end
+
+function creditsScreen.rightButtonDown()
+ if creditsScreen.current < 3 then
+ creditsScreen.current += 1
+ creditsScreen.updateText()
+ else
+ creditsScreen.close()
+ end
+end
+
+function creditsScreen.close()
+ pd.inputHandlers.pop()
+ pd.update = creditsScreen.oldUpdate
+
+ inScreen = false
+end
+
diff --git a/src/ui/screens/filepicker.lua b/src/ui/screens/filepicker.lua
new file mode 100644
index 0000000..003c289
--- /dev/null
+++ b/src/ui/screens/filepicker.lua
@@ -0,0 +1,283 @@
+-- file picker
+
+filePicker = { cranked = function() end }
+filePicker.selectedFile = nil
+filePicker.callback = nil
+filePicker.oldUpdate = nil
+filePicker.mode = nil
+filePicker.sample = snd.sampleplayer.new(snd.sample.new(1))
+filePicker.keyTimer = nil
+filePicker.animator = nil
+filePicker.fileMetadataImage = nil
+
+local dirs = {}
+local currentPath = "/"
+local row = filePickList:getSelectedRow()
+local songMeta = ""
+local currentText = ""
+local songMetaName = ""
+dirLocations = {}
+
+function filePicker.open(callback, mode)
+ inScreen = true
+
+ row = filePickList:getSelectedRow()
+
+ if callback ~= nil then
+ filePicker.callback = callback
+ end
+ filePicker.mode = mode
+
+ dirs = {}
+ if mode == "song" then
+ currentPath = "/songs/"
+ table.insert(dirs, "/")
+ table.insert(dirLocations, 2)
+
+ applyMenuItems("songpicker")
+ else
+ currentPath = "/"
+ end
+
+ filePicker.anim()
+
+ pd.inputHandlers.push(filePicker, true)
+ filePicker.oldUpdate = pd.update
+ pd.update = filePicker.update
+
+ filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
+ filePickList:setSelectedRow(1)
+ filePickList:scrollToRow(1)
+end
+
+function filePicker.update(force)
+ local ftype = "sample"
+ row = filePickList:getSelectedRow()
+ currentText = filePickListContents[row]
+
+ if filePicker.mode == "song" then
+ ftype = "song"
+ end
+
+ if filePickList.needsDisplay == true or (filePicker.animator ~= nil and filePicker.animator:ended() == false) or force then
+ filePickList:drawInRect(filePicker.animator:currentValue(), 0, 400, 240)
+ fnt8x8:drawTextAligned("choose a " .. ftype, 200, 0, align.center)
+ end
+
+ if pd.buttonIsPressed("right") and filePicker.mode == "song" and string.sub(currentText, #currentText - 5) == "(song)" then
+ if songMeta == "" or songMetaName ~= currentText then
+ local songName = string.unnormalize(string.sub(currentText, 1, #currentText - 8))
+ local songData = pd.datastore.read(currentPath .. songName .. "/song")
+ local rect = nil
+
+ if songData ~= nil then
+ local timeStuff = songData[3][2]
+
+ local concatTable = {
+ songName,
+ " by ",
+ songData[3][1],
+ "\n\n",
+ "tempo: ",
+ songData[2][1] * 8,
+ ", ",
+ songData[2][2],
+ " steps",
+ "\n\n",
+ "last modified: ",
+ timeStuff["month"],
+ "/",
+ timeStuff["day"],
+ "/",
+ timeStuff["year"],
+ " ",
+ timeStuff["hour"],
+ ":",
+ timeStuff["minute"],
+ ":",
+ timeStuff["second"]
+ }
+
+ songMeta = string.normalize(table.concat(concatTable))
+ songMetaName = currentText
+
+ local textw, texth = gfx.getTextSizeForMaxWidth(songMeta, 320)
+ filePicker.fileMetadataImage = gfx.image.new(400, 240, gfx.kColorClear)
+
+ gfx.pushContext(filePicker.fileMetadataImage)
+
+ rect = pd.geometry.rect.new(40, 120 - (texth / 2), 320, texth + 4)
+
+ gfx.fillRect(rect)
+ gfx.pushContext()
+ gfx.setColor(gfx.kColorXOR)
+ gfx.drawRect(rect)
+ gfx.popContext()
+
+ rect.y += 1
+
+ gfx.drawTextInRect(songMeta, rect, nil, nil, kTextAlignment.center)
+
+ rect.y -= 1
+
+ gfx.popContext()
+ end
+ end
+
+ gfx.setImageDrawMode(gfx.kDrawModeCopy)
+ filePicker.fileMetadataImage:draw(0, 0)
+ gfx.setImageDrawMode(gfx.kDrawModeNXOR)
+ else
+ songMeta = ""
+ songMetaName = ""
+ end
+
+ pd.timer.updateTimers()
+end
+
+function filePicker.close()
+ pd.inputHandlers.pop()
+ pd.update = filePicker.oldUpdate
+ filePicker.callback(filePicker.selectedFile)
+ filePicker.animator = nil
+ inScreen = false
+end
+
+function filePicker.AButtonDown()
+ if pd.file.isdir(currentPath .. filePickListContents[row]) then
+ filePicker.anim()
+ table.insert(dirs, currentPath)
+ table.insert(dirLocations, row)
+ currentPath = currentPath .. filePickListContents[row]
+ filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
+ filePickList:setSelectedRow(1)
+ filePickList:scrollToRow(1)
+ elseif filePickListContents[row] == "*Ā*" then
+ filePicker.anim(true)
+ currentPath = table.remove(dirs)
+ filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
+ local rmed = table.remove(dirLocations)
+ filePickList:setSelectedRow(rmed)
+ filePickList:scrollToRow(rmed)
+ elseif filePickListContents[row] == "*Ą* record sample" then
+ sampleScreen.open(function(sample)
+ filePicker.selectedFile = sample
+ filePicker.close()
+ end)
+ elseif filePickListContents[row] == "*ċ* edit sample" then
+ sampleEditScreen.open(snd.sample.new("temp/" .. listview:getSelectedRow() .. ".pda"), function(sample)
+ filePicker.selectedFile = sample
+ filePicker.close()
+ end, pd.datastore.readImage("temp/" .. listview:getSelectedRow() .. ".pdi"))
+ else
+ if filePicker.mode ~= "song" or string.find(filePickListContents[row], "%/ %(song%)") then
+ filePicker.selectedFile = string.unnormalize(currentPath .. filePickListContents[row])
+ filePicker.close()
+ end
+ end
+end
+
+function filePicker.BButtonDown()
+ if currentPath == "/" then
+ filePicker.selectedFile = "none"
+ filePicker.close()
+ else
+ currentPath = table.remove(dirs)
+ filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
+ local rmed = table.remove(dirLocations)
+ filePickList:setSelectedRow(rmed)
+ filePickList:scrollToRow(rmed)
+ filePicker.anim(true)
+ end
+end
+
+function filePicker.rightButtonDown()
+ if not pd.file.isdir(currentPath .. filePickListContents[row]) and string.find(filePickListContents[row], "%.pda") then
+ filePicker.sample:stop()
+ filePicker.sample:setSample(snd.sample.new(string.unnormalize(currentPath .. filePickListContents[row])))
+ filePicker.sample:play()
+ end
+end
+
+function filePicker.rightButtonUp()
+ if filePicker.mode == "song" then
+ filePicker.update(true)
+ end
+end
+
+function filePicker.upButtonDown()
+ filePicker.upButtonUp()
+
+ local function callback()
+ filePickList:selectPreviousRow()
+ end
+ filePicker.upKeyTimer = pd.timer.keyRepeatTimerWithDelay(300, 75, callback)
+end
+
+function filePicker.downButtonDown()
+ filePicker.downButtonUp()
+
+ local function callback()
+ filePickList:selectNextRow()
+ end
+ filePicker.downKeyTimer = pd.timer.keyRepeatTimerWithDelay(300, 75, callback)
+end
+
+function filePicker.upButtonUp()
+ if filePicker.upKeyTimer ~= nil then
+ filePicker.upKeyTimer:remove()
+ end
+end
+
+function filePicker.downButtonUp()
+ if filePicker.downKeyTimer ~= nil then
+ filePicker.downKeyTimer:remove()
+ end
+end
+
+function filePicker.anim(back)
+ gfx.clear()
+ if back then
+ filePicker.animator = gfx.animator.new(200, -200, 0, pd.easingFunctions.outQuart)
+ else
+ filePicker.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
+ end
+end
+
+function filePicker.modifyDirContents(val)
+ if currentPath == "/" then
+ for i = #val, 1, -1 do
+ if val[i] ~= "samples/" and val[i] ~= "temp/" and val[i] ~= "songs/" then
+ table.remove(val, i)
+ end
+ end
+ end
+
+ for i = 1, #val do
+ if pd.file.isdir(currentPath .. val[i]) then
+ if filePicker.mode == "song" and table.indexOfElement(pd.file.listFiles(currentPath .. val[i]), "song.json") ~= nil then
+ val[i] = val[i] .. " (song)"
+ end
+ end
+ val[i] = string.normalize(val[i])
+ end
+
+ if currentPath ~= "/" then
+ table.insert(val, 1, "*Ā*")
+ elseif filePicker.mode ~= "song" then
+ table.insert(val, 1, "*Ą* record sample")
+ end
+
+ if filePicker.mode == "newsmp" and currentPath == "/" then
+ table.insert(val, 1, "*ċ* edit sample")
+ end
+
+ return val
+end
+
+function filePicker.updateFiles()
+ filePickList:set(filePicker.modifyDirContents(pd.file.listFiles(currentPath)))
+ filePickList:setSelectedRow(1)
+ filePickList:scrollToRow(1)
+end
+
diff --git a/src/ui/screens/keyboard.lua b/src/ui/screens/keyboard.lua
new file mode 100644
index 0000000..af76625
--- /dev/null
+++ b/src/ui/screens/keyboard.lua
@@ -0,0 +1,86 @@
+-- keyboard for text input
+
+keyboardScreen = {}
+keyboardScreen.oldUpdate = nil
+keyboardScreen.callback = nil
+keyboardScreen.askingForOK = false
+keyboardScreen.prompt = ""
+keyboardScreen.text = ""
+keyboardScreen.limit = nil
+keyboardScreen.origtext = ""
+
+function keyboardScreen.open(prompt, text, limit, callback)
+ inScreen = true
+
+ text = string.normalize(text)
+
+ keyboardScreen.callback = callback
+ keyboardScreen.text = text
+ keyboardScreen.origtext = text
+ keyboardScreen.prompt = prompt
+ keyboardScreen.askingForOK = false
+
+ if limit == nil then
+ limit = 100000
+ end
+ keyboardScreen.limit = limit
+
+ pd.inputHandlers.push(keyboardScreen, true)
+ keyboardScreen.oldUpdate = pd.update
+ pd.update = keyboardScreen.update
+
+ pd.keyboard.show(string.unnormalize(text))
+end
+
+function keyboardScreen.update()
+ local rectWidth = 400 - pd.keyboard.width()
+ gfx.clear()
+
+ if keyboardScreen.askingForOK == false then
+ gfx.drawTextInRect(keyboardScreen.prompt, 0, 0, rectWidth, 100, nil, nil, align.center)
+ gfx.drawTextInRect(keyboardScreen.text, 0, 104, rectWidth, 136, nil, nil, align.center)
+ else
+ gfx.drawTextInRect("is this good?\n\na to confirm, b to redo, left to quit", 0, 0, rectWidth, 100, nil, nil,
+ align.center)
+ gfx.drawTextInRect(keyboardScreen.text, 0, 104, rectWidth, 136, nil, nil, align.center)
+ end
+end
+
+function keyboardScreen.close()
+ pd.inputHandlers.pop()
+ pd.update = keyboardScreen.oldUpdate
+
+ inScreen = false
+
+ keyboardScreen.callback(string.unnormalize(keyboardScreen.text))
+end
+
+function pd.keyboard.keyboardWillHideCallback()
+ keyboardScreen.askingForOK = true
+end
+
+function pd.keyboard.textChangedCallback()
+ if #pd.keyboard.text <= keyboardScreen.limit then
+ keyboardScreen.text = string.normalize(pd.keyboard.text)
+ else
+ pd.keyboard.text = string.sub(pd.keyboard.text, 1, keyboardScreen.limit)
+ end
+end
+
+function keyboardScreen.AButtonDown()
+ if keyboardScreen.askingForOK == true then
+ keyboardScreen.close()
+ end
+end
+
+function keyboardScreen.BButtonDown()
+ if keyboardScreen.askingForOK == true then
+ keyboardScreen.askingForOK = false
+ pd.keyboard.show(string.normalize(keyboardScreen.text))
+ end
+end
+
+function keyboardScreen.leftButtonDown()
+ keyboardScreen.text = "_EXITED_KEYBOĀRD"
+ keyboardScreen.close()
+end
diff --git a/src/ui/screens/log.lua b/src/ui/screens/log.lua
new file mode 100644
index 0000000..7ca054d
--- /dev/null
+++ b/src/ui/screens/log.lua
@@ -0,0 +1,32 @@
+-- log screen, for the cools haha
+
+logScreen = {}
+logScreen.log = {}
+
+function logScreen.init()
+ logScreen.log = {}
+end
+
+function logScreen.append(text)
+ if settings["logscreens"] == true then
+ gfx.clear()
+
+ table.insert(logScreen.log, text)
+
+ if #logScreen.log > 30 then
+ table.remove(logScreen.log, 1)
+ end
+
+ for index, value in ipairs(logScreen.log) do
+ local yval = (index - 1) * 8
+
+ if index == 1 then
+ yval = 0
+ end
+
+ rains1x:drawText(value, 2, yval)
+ end
+
+ pd.display.flush()
+ end
+end
diff --git a/src/ui/screens/sample.lua b/src/ui/screens/sample.lua
new file mode 100644
index 0000000..7846768
--- /dev/null
+++ b/src/ui/screens/sample.lua
@@ -0,0 +1,196 @@
+-- sampler
+
+sampleScreen = {}
+sampleScreen.sample = nil
+sampleScreen.callback = nil
+sampleScreen.recording = false
+sampleScreen.oldUpdate = nil
+sampleScreen.waiting = false
+sampleScreen.waitForButton = false
+sampleScreen.recAt = 0.15
+sampleScreen.recTimer = pd.timer.new(5000)
+sampleScreen.waveformImage = nil
+sampleScreen.waveformAnimator = nil
+sampleScreen.waveformLastXY = { 0, 20 }
+
+local state = "press A to arm..."
+
+function sampleScreen.open(callback)
+ inScreen = true
+
+ sampleScreen.sample = nil
+ sampleScreen.callback = nil
+ sampleScreen.recording = false
+ sampleScreen.oldUpdate = nil
+ sampleScreen.waiting = false
+ sampleScreen.waitForButton = false
+ sampleScreen.recTimer = pd.timer.new(5000)
+ sampleScreen.recTimer:reset()
+ sampleScreen.recTimer:pause()
+ sampleScreen.waveformImage = gfx.image.new(400, 45)
+ sampleScreen.waveformLastXY = { 0, 20 }
+ state = "press A to arm..."
+
+
+ if callback ~= nil then
+ sampleScreen.callback = callback
+ end
+
+ if settings["stoponsample"] == true then
+ seq:stop()
+ seq:allNotesOff()
+ end
+
+ pd.inputHandlers.push(sampleScreen, true)
+ sampleScreen.oldUpdate = pd.update
+ pd.update = sampleScreen.update
+ snd.micinput.startListening()
+end
+
+function sampleScreen.update()
+ gfx.clear()
+ if sampleScreen.waiting == true and snd.micinput.getLevel() > sampleScreen.recAt then
+ state = "recording..."
+ sampleScreen.waveformAnimator = gfx.animator.new(5000, 1, 400)
+ sampleScreen.record()
+ end
+
+ if sampleScreen.recording == true then
+ local lastxy = sampleScreen.waveformLastXY
+ local x = sampleScreen.waveformAnimator:currentValue()
+ local y = 40 + ((-snd.micinput.getLevel()) * 40)
+ gfx.pushContext(sampleScreen.waveformImage)
+ gfx.drawLine(lastxy[1], lastxy[2], x, y)
+ gfx.popContext()
+ sampleScreen.waveformLastXY = { x, y }
+ sampleScreen.waveformImage:draw(0, 55)
+ end
+
+ gfx.drawTextAligned(state, 200, 0, align.center)
+ gfx.drawRect(50, 110, 300, 20)
+ gfx.fillRect(50, 110, snd.micinput.getLevel() * 300, 20)
+ fnt8x8:drawTextAligned(math.round(snd.micinput.getLevel(), 2), 200, 116, align.center)
+ fnt8x8:drawTextAligned("will start recording from " .. snd.micinput.getSource() .. " if volume = " ..
+ sampleScreen.recAt, 200, 231, align.center)
+ gfx.drawTextAligned(tostring(sampleScreen.recTimer.currentTime / 1000) .. " / 5.0", 200, 20, align.center)
+ pd.timer.updateTimers()
+end
+
+function sampleScreen.close()
+ pd.inputHandlers.pop()
+ pd.update = sampleScreen.oldUpdate
+ if settings["saveWaveforms"] == true then
+ sampleScreen.callback({ sampleScreen.sample, sampleScreen.waveformImage:copy() })
+ else
+ sampleScreen.callback(sampleScreen.sample)
+ end
+
+ inScreen = false
+
+ if settings["stoponsample"] == true then
+ seq:play()
+ end
+end
+
+function sampleScreen.record()
+ sampleScreen.recording = true
+ sampleScreen.waiting = false
+ sampleScreen.recTimer:reset()
+ sampleScreen.recTimer:start()
+
+ local format = snd.kFormat16bitMono
+
+ if settings["sample16bit"] == false then
+ format = snd.kFormat8bitMono
+ end
+
+ local buffer = snd.sample.new(5, format)
+ snd.micinput.recordToSample(buffer, function(smp)
+ sampleScreen.sample = smp
+ snd.micinput.stopListening()
+ if sampleScreen.sample == "none" then
+ goto continue
+ end
+
+ sampleScreen.recording = false
+ sampleScreen.waitForButton = true
+
+ gfx.clear()
+ smp:play()
+ gfx.drawTextInRect("save?\n\na to save, b to redo, right to hear again", 20, 85, 360, 200, nil, nil, align.center)
+ sampleScreen.waveformImage:drawCentered(400 - (sampleScreen.waveformAnimator:currentValue() / 2), 45)
+ pd.stop()
+ ::continue::
+ end)
+end
+
+function sampleScreen.AButtonDown()
+ if sampleScreen.waitForButton == true then
+ pd.start()
+ displayInfo("saved as " .. listview:getSelectedRow() .. ".pda")
+ sampleScreen.sample:save(songdir .. listview:getSelectedRow() .. ".pda")
+ if settings["savewavs"] == true then
+ sampleScreen.sample:save(songdir .. listview:getSelectedRow() .. ".wav")
+ end
+ sampleScreen.close()
+ elseif sampleScreen.waiting == false and sampleScreen.recording == false then
+ sampleScreen.waiting = true
+ state = "armed, waiting..."
+ else
+ snd.micinput.stopRecording()
+ end
+end
+
+function sampleScreen.BButtonDown()
+ if sampleScreen.waitForButton == true then
+ pd.start()
+ sampleScreen.sample = nil
+ sampleScreen.recording = false
+ sampleScreen.oldUpdate = nil
+ sampleScreen.waiting = false
+ sampleScreen.waitForButton = false
+ sampleScreen.recTimer:reset()
+ sampleScreen.recTimer:pause()
+ sampleScreen.waveformImage = gfx.image.new(400, 45)
+ sampleScreen.waveformAnimator = nil
+ sampleScreen.waveformLastXY = { 0, 20 }
+
+ state = "press A to arm..."
+
+ snd.micinput.startListening()
+ elseif sampleScreen.recording == false then
+ sampleScreen.sample = "none"
+ snd.micinput.stopListening()
+ sampleScreen.close()
+ end
+end
+
+function sampleScreen.upButtonDown()
+ sampleScreen.recAt += 0.05
+ sampleScreen.fixRec()
+end
+
+function sampleScreen.rightButtonDown()
+ if sampleScreen.waitForButton == true then
+ sampleScreen.sample:play()
+ else
+ sampleScreen.recAt += 0.01
+ sampleScreen.fixRec()
+ end
+end
+
+function sampleScreen.downButtonDown()
+ sampleScreen.recAt -= 0.05
+ sampleScreen.fixRec()
+end
+
+function sampleScreen.leftButtonDown()
+ sampleScreen.recAt -= 0.01
+ sampleScreen.fixRec()
+end
+
+function sampleScreen.fixRec()
+ sampleScreen.recAt = math.round(sampleScreen.recAt, 2)
+ sampleScreen.recAt = math.max(0.0, math.min(1.0, (sampleScreen.recAt)))
+end
+
diff --git a/src/ui/screens/sampleedit.lua b/src/ui/screens/sampleedit.lua
new file mode 100644
index 0000000..b9b24fe
--- /dev/null
+++ b/src/ui/screens/sampleedit.lua
@@ -0,0 +1,132 @@
+-- sample editor
+
+sampleEditScreen = {}
+sampleEditScreen.oldUpdate = nil
+sampleEditScreen.sample = nil
+sampleEditScreen.editedSample = nil
+sampleEditScreen.callback = nil
+sampleEditScreen.changeVal = 1000
+sampleEditScreen.sampleLen = 0
+sampleEditScreen.trim = { 0, 0 }
+sampleEditScreen.side = 1 -- 1 = begin, 2 = end
+sampleEditScreen.ctrPixel = 0
+
+function sampleEditScreen.open(sample, callback, image)
+ inScreen = true
+
+ sampleEditScreen.sample = sample
+ sampleEditScreen.sampleImg = image
+ sampleEditScreen.editedSample = nil
+ sampleEditScreen.callback = callback
+ sampleEditScreen.changeVal = 1000
+ sampleEditScreen.trim = { 0, 0 }
+ sampleEditScreen.side = 1
+
+ if image ~= nil then
+ local done = false
+ for x = 400, 0, -1 do
+ for y = 40, 0, -1 do
+ if image:sample(x, y) == gfx.kColorBlack then
+ sampleEditScreen.ctrPixel = 400 - (x / 2)
+ done = true
+ break
+ end
+ end
+ if done then
+ break
+ end
+ end
+ end
+
+ sampleEditScreen.sampleLen = math.round(sample:getLength() * 44100, 0)
+
+ sampleEditScreen.trim[2] = sampleEditScreen.sampleLen
+
+ sampleEditScreen.editedSample = sampleEditScreen.sample:getSubsample(sampleEditScreen.trim[1], sampleEditScreen.trim
+ [2])
+ sampleEditScreen.samplePlayer = snd.sampleplayer.new(sampleEditScreen.editedSample)
+
+ pd.inputHandlers.push(sampleEditScreen, true)
+ sampleEditScreen.oldUpdate = pd.update
+ pd.update = sampleEditScreen.update
+
+ gfx.clear()
+
+ sample:play()
+end
+
+function sampleEditScreen.update()
+ local sidetext = "start"
+ gfx.clear()
+ local crank = pd.getCrankTicks(settings["cranksens"])
+ local side = sampleEditScreen.side
+
+ if crank ~= 0 then
+ local otherside = 1 -- IS THAT A MINECRAFT REFERENCE????? :OOOO
+ sampleEditScreen.trim[side] += sampleEditScreen.changeVal * crank
+ sampleEditScreen.trim[side] = math.normalize(sampleEditScreen.trim[side], 0, sampleEditScreen.sampleLen)
+
+ if side == 1 then
+ otherside = 2
+ if sampleEditScreen.trim[side] >= sampleEditScreen.trim[otherside] then
+ sampleEditScreen.trim[side] = sampleEditScreen.trim[otherside] - 10
+ end
+ else
+ if sampleEditScreen.trim[side] <= sampleEditScreen.trim[otherside] then
+ sampleEditScreen.trim[side] = sampleEditScreen.trim[otherside] + 10
+ end
+ end
+ sampleEditScreen.editedSample = sampleEditScreen.sample:getSubsample(sampleEditScreen.trim[1],
+ sampleEditScreen.trim[2])
+ sampleEditScreen.samplePlayer:setSample(sampleEditScreen.editedSample)
+ sampleEditScreen.samplePlayer:play()
+ end
+
+ if sampleEditScreen.side == 2 then
+ sidetext = "end"
+ end
+
+ gfx.drawTextAligned("start: " .. sampleEditScreen.trim[1] .. ", end: " .. sampleEditScreen.trim[2], 200, 104,
+ align.center)
+ gfx.drawTextAligned("selected: " .. sidetext, 200, 120, align.center)
+ gfx.drawTextAligned("a to save, b to discard", 200, 210, align.center)
+ fnt8x8:drawTextAligned("changing frames by " .. sampleEditScreen.changeVal, 200, 231, align.center)
+
+ if sampleEditScreen.sampleImg ~= nil then
+ sampleEditScreen.sampleImg:drawCentered(sampleEditScreen.ctrPixel, 40)
+ end
+end
+
+function sampleEditScreen.rightButtonDown()
+ sampleEditScreen.side = 2
+end
+
+function sampleEditScreen.leftButtonDown()
+ sampleEditScreen.side = 1
+end
+
+function sampleEditScreen.upButtonDown()
+ sampleEditScreen.changeVal = math.normalize(sampleEditScreen.changeVal + 50, 50, 2000)
+end
+
+function sampleEditScreen.downButtonDown()
+ sampleEditScreen.changeVal = math.normalize(sampleEditScreen.changeVal - 50, 50, 2000)
+end
+
+function sampleEditScreen.close(sample)
+ pd.inputHandlers.pop()
+ pd.update = sampleEditScreen.oldUpdate
+
+ inScreen = false
+
+ sampleEditScreen.callback(sample)
+end
+
+function sampleEditScreen.BButtonDown()
+ sampleEditScreen.close(sampleEditScreen.sample)
+end
+
+function sampleEditScreen.AButtonDown()
+ sampleEditScreen.close(sampleEditScreen.editedSample)
+end
+
diff --git a/src/ui/screens/settings.lua b/src/ui/screens/settings.lua
new file mode 100644
index 0000000..8120422
--- /dev/null
+++ b/src/ui/screens/settings.lua
@@ -0,0 +1,382 @@
+-- settings
+
+settingsScreen = {}
+settingsScreen.subMenu = ""
+settingsScreen.oldUpdate = nil
+settingsScreen.animator = nil
+settingsScreen.locationStack = {}
+
+local crankDockList = { "pattern", "track", "fx", "song", "none" }
+
+function settingsScreen.updateOutputs()
+ if settings["output"] < 3 then
+ settings["output"] = settings["output"] + 1
+ else
+ settings["output"] = 0
+ snd.setOutputsActive(false, true)
+ end
+ if settings["output"] == 1 then
+ snd.setOutputsActive(true, false)
+ elseif settings["output"] == 2 then
+ snd.setOutputsActive(true, true)
+ elseif settings["output"] == 3 then
+ local state = snd.getHeadphoneState()
+ snd.setOutputsActive(state, not state)
+ end
+end
+
+function settingsScreen.open()
+ inScreen = true
+
+ settingsScreen.updateSettings()
+
+ pd.getSystemMenu():removeAllMenuItems()
+
+ settingsList:setSelectedRow(1)
+ settingsList:scrollToRow(1)
+
+ settingsScreen.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
+
+ pd.inputHandlers.push(settingsScreen, true)
+ settingsScreen.oldUpdate = pd.update
+ pd.update = settingsScreen.update
+end
+
+function settingsScreen.update()
+ if settingsList.needsDisplay or settingsScreen.animator:ended() == false
+ then
+ gfx.clear()
+ settingsList:drawInRect(settingsScreen.animator:currentValue(), 0, 400, 240)
+
+ if settingsScreen.subMenu == "" then
+ fnt8x8:drawTextAligned("settings", 200, 0, align.center)
+ else
+ fnt8x8:drawTextAligned("settings/" .. string.sub(settingsScreen.subMenu, 1, #settingsScreen.subMenu - 1), 200, 0,
+ align.center)
+ end
+ fnt8x8:drawTextAligned("cs-16 version " .. pd.metadata.version .. ", build " .. pd.metadata.buildNumber, 200, 231,
+ align.center)
+ end
+
+ pd.timer.updateTimers()
+end
+
+function settingsScreen.close()
+ settingsScreen.animator = nil
+ pd.inputHandlers.pop()
+ pd.update = settingsScreen.oldUpdate
+
+ applyMenuItems("song")
+
+ inScreen = false
+end
+
+function settingsScreen.upFolder()
+ local oldmenu = settingsScreen.subMenu
+
+ if oldmenu == "ui/visualizer/" then
+ settingsScreen.subMenu = "ui/"
+ else
+ settingsScreen.subMenu = ""
+ end
+ settingsScreen.updateSettings()
+ settingsList:setSelectedRow(table.remove(settingsScreen.locationStack))
+ settingsList:scrollToTop()
+ settingsScreen.animator = gfx.animator.new(200, -200, 0, pd.easingFunctions.outQuart)
+end
+
+function settingsScreen.downButtonDown()
+ settingsList:selectNextRow()
+end
+
+function settingsScreen.upButtonDown()
+ settingsList:selectPreviousRow()
+end
+
+function settingsScreen.leftButtonDown()
+ local row = settingsList:getSelectedRow()
+
+ if row == 3 and settingsScreen.subMenu == "general/" then
+ settingsScreen.updateOutputs()
+ elseif row == 4 and settingsScreen.subMenu == "general/" then
+ settings["cranksens"] = table.cycle(crankSensList, settings["cranksens"], true)
+ elseif row == 6 and settingsScreen.subMenu == "behavior/" then
+ settings["crankDockedScreen"] = table.cycle(crankDockList, settings["crankDockedScreen"], true)
+ end
+ settingsScreen.updateSettings()
+end
+
+function settingsScreen.rightButtonDown()
+ local row = settingsList:getSelectedRow()
+
+ if row == 3 and settingsScreen.subMenu == "general/" then
+ settingsScreen.updateOutputs()
+ elseif row == 4 and settingsScreen.subMenu == "general/" then
+ settings["cranksens"] = table.cycle(crankSensList, settings["cranksens"])
+ elseif row == 6 and settingsScreen.subMenu == "behavior/" then
+ settings["crankDockedScreen"] = table.cycle(crankDockList, settings["crankDockedScreen"])
+ end
+ settingsScreen.updateSettings()
+end
+
+function settingsScreen.BButtonDown()
+ if settingsScreen.subMenu == "" then
+ settingsScreen.close()
+ else
+ settingsScreen.upFolder()
+ end
+end
+
+function settingsScreen.AButtonDown()
+ local row = settingsList:getSelectedRow()
+ local text = settingsListContents[settingsList:getSelectedRow()]
+
+ local refresh = true
+
+ if settingsScreen.subMenu == "" then
+ settingsScreen.subMenu = text
+ settingsList:setSelectedRow(1)
+ settingsScreen.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
+
+ table.insert(settingsScreen.locationStack, row)
+ else
+ if text == "*Ā*" then
+ settingsScreen.upFolder()
+ refresh = false
+ end
+
+ if settingsScreen.subMenu == "ui/" then
+ if row == 2 then
+ settings["dark"] = not settings["dark"]
+ pd.display.setInverted(settings["dark"])
+ elseif row == 3 then
+ settingsScreen.subMenu = "ui/visualizer/"
+ settingsList:setSelectedRow(1)
+ settingsScreen.animator = gfx.animator.new(200, 200, 0, pd.easingFunctions.outQuart)
+
+ table.insert(settingsScreen.locationStack, row)
+ elseif row == 4 then
+ settings["num/max"] = not settings["num/max"]
+ elseif row == 5 then
+ settings["showNoteNames"] = not settings["showNoteNames"]
+ elseif row == 6 then
+ settings["screenAnimation"] = not settings["screenAnimation"]
+ elseif row == 7 then
+ settings["useSystemFont"] = not settings["useSystemFont"]
+ if settings["useSystemFont"] == true then
+ fnt = rains2x
+ fnt8x8 = rains1x
+ else
+ fnt = gfx.font.new("fnt/modified-tron")
+ fnt8x8 = gfx.font.new("fnt/modified-tron-8x8")
+ end
+ gfx.setFont(fnt)
+
+ buttons = {
+ Button(5, 5, nil, nil, "back", true),
+ Button(333 - (fnt8x8:getTextWidth("toggle") / 2), 53, nil, nil, "toggle", true),
+ Button(53 - (fnt8x8:getTextWidth("select") / 2), 125, nil, nil, "select", true),
+ Button(143 - (fnt8x8:getTextWidth("play") / 2), 125, nil, nil, "play", true)
+ }
+
+ allElems = { buttons[1], knobs[1], knobs[2], knobs[3], knobs[4], knobs[5], buttons[2], buttons[3], buttons[4],
+ knobs[6], knobs[7], knobs[8], knobs[9] }
+ elseif row == 8 then
+ settings["logscreens"] = not settings["logscreens"]
+ elseif row == 9 then
+ settings["fxvfx"] = not settings["fxvfx"]
+ elseif row == 10 then
+ if settings["50fps"] == false then
+ messageBox.open(
+ "\n\nwarning!\n\nrunning cs-16 at 50fps will reduce your battery life, but improve performance.\n\nare you sure you want to enable this?\n\na = yes, b = no",
+ function(ans)
+ if ans == "yes" then
+ settings["50fps"] = not settings["50fps"]
+ if settings["50fps"] == true then
+ pd.display.setRefreshRate(50)
+ else
+ pd.display.setRefreshRate(30)
+ end
+ end
+ settingsScreen.updateSettings()
+ end)
+ else
+ settings["50fps"] = not settings["50fps"]
+ pd.display.setRefreshRate(30)
+ end
+ end
+ elseif settingsScreen.subMenu == "ui/visualizer/" then
+ if text ~= "----- external -----" then
+ text = string.split(text, ": ")[1]
+
+ settings["visualizer"][text] = not settings["visualizer"][text]
+ end
+ elseif settingsScreen.subMenu == "behavior/" then
+ if row == 2 then
+ settings["playonload"] = not settings["playonload"]
+ elseif row == 3 then
+ settings["stoponsample"] = not settings["stoponsample"]
+ elseif row == 4 then
+ settings["stopontempo"] = not settings["stopontempo"]
+ elseif row == 5 then
+ settings["savewavs"] = not settings["savewavs"]
+ elseif row == 6 then
+ settings["crankDockedScreen"] = table.cycle(crankDockList, settings["crankDockedScreen"])
+ end
+ elseif settingsScreen.subMenu == "recording/" then
+ if row == 8 then
+ local quant = settings["recordQuantization"]
+ if quant == 1 then -- every #th note
+ settings["recordQuantization"] = 2
+ elseif quant == 2 then
+ settings["recordQuantization"] = 4
+ elseif quant == 4 then
+ settings["recordQuantization"] = 1
+ end
+ elseif row > 1 then
+ keyboardScreen.open("enter a new track number for this button (1-16):", "", 2, function(t)
+ local num = tonumber(t)
+ if num ~= nil then
+ if num > 0 and num < 17 then
+ if row == 2 then
+ settings["aRecTrack"] = num
+ elseif row == 3 then
+ settings["bRecTrack"] = num
+ elseif row == 4 then
+ settings["upRecTrack"] = num
+ elseif row == 5 then
+ settings["downRecTrack"] = num
+ elseif row == 6 then
+ settings["leftRecTrack"] = num
+ elseif row == 7 then
+ settings["rightRecTrack"] = num
+ end
+ settingsScreen.updateSettings()
+ end
+ end
+ end)
+ end
+ elseif settingsScreen.subMenu == "general/" then
+ if row == 2 then
+ keyboardScreen.open("enter new author name:", settings["author"], 15, function(t)
+ if t ~= "_EXITED_KEYBOĀRD" then
+ settings["author"] = t
+ if songdir == "temp/" then
+ songAuthor = t
+ end
+ settingsScreen.updateSettings()
+ end
+ end)
+ elseif row == 3 then
+ settingsScreen.updateOutputs()
+ elseif row == 4 then
+ settings["cranksens"] = table.cycle(crankSensList, settings["cranksens"])
+ elseif row == 5 then
+ creditsScreen.open()
+ end
+ elseif settingsScreen.subMenu == "sampling/" then
+ if row == 2 then
+ settings["sample16bit"] = not settings["sample16bit"]
+ elseif row == 3 then
+ settings["saveWaveforms"] = not settings["saveWaveforms"]
+ end
+ end
+ end
+
+ if refresh then
+ settingsScreen.updateSettings()
+ end
+end
+
+function settingsScreen.updateSettings()
+ if settingsScreen.subMenu == "" then
+ settingsList:set({ "general/", "behavior/", "recording/", "sampling/", "ui/" })
+ elseif settingsScreen.subMenu == "general/" then
+ local outputText = "speaker"
+ if settings["output"] == 1 then
+ outputText = "headset"
+ elseif settings["output"] == 2 then
+ outputText = "speaker, headset"
+ elseif settings["output"] == 3 then
+ outputText = "auto"
+ end
+
+ settingsList:set({
+ "*Ā*",
+ "author: " .. settings["author"],
+ "output: " .. outputText,
+ "crank speed: " .. settings["cranksens"],
+ "credits..."
+ })
+ elseif settingsScreen.subMenu == "behavior/" then
+ local screen = settings["crankDockedScreen"]
+
+ if screen == "pattern" then
+ screen = "ptn"
+ elseif screen == "track" then
+ screen = "trk"
+ end
+
+ settingsList:set({
+ "*Ā*",
+ "play on load: " .. tostring(settings["playonload"]),
+ "stop if sampling: " .. tostring(settings["stoponsample"]),
+ "tempo edit stop: " .. tostring(settings["stopontempo"]),
+ "save .wav samples: " .. tostring(settings["savewavs"]),
+ "crank dock screen: " .. tostring(screen)
+ })
+ elseif settingsScreen.subMenu == "recording/" then
+ settingsList:set({
+ "*Ā*",
+ "_Ⓐ_ button track: " .. tostring(settings["aRecTrack"]),
+ "_Ⓑ_ button track: " .. tostring(settings["bRecTrack"]),
+ "_⬆️_ button track: " .. tostring(settings["upRecTrack"]),
+ "_⬇️_ button track: " .. tostring(settings["downRecTrack"]),
+ "_⬅️_ button track: " .. tostring(settings["leftRecTrack"]),
+ "_➡️_ button track: " .. tostring(settings["rightRecTrack"]),
+ "quantization: " .. tostring(settings["recordQuantization"])
+ })
+ elseif settingsScreen.subMenu == "sampling/" then
+ local format = "16 bit"
+ if settings["sample16bit"] == false then
+ format = "8 bit"
+ end
+ settingsList:set({
+ "*Ā*",
+ "sample format: " .. format,
+ "save waveforms: " .. tostring(settings["saveWaveforms"])
+ })
+ elseif settingsScreen.subMenu == "ui/" then
+ settingsList:set({
+ "*Ā*",
+ "dark mode: " .. tostring(settings["dark"]),
+ "visualizer...",
+ "show number/total: " .. tostring(settings["num/max"]),
+ "show note names: " .. tostring(settings["showNoteNames"]),
+ "animate scrn move: " .. tostring(settings["screenAnimation"]),
+ "use system font: " .. tostring(settings["useSystemFont"]),
+ "show log screens: " .. tostring(settings["logscreens"]),
+ "fx screen vfx: " .. tostring(settings["fxvfx"]),
+ "50fps: " .. tostring(settings["50fps"])
+ })
+ elseif settingsScreen.subMenu == "ui/visualizer/" then
+ local tempSet = {
+ "*Ā*",
+ "sine: " .. tostring(settings["visualizer"]["sine"]),
+ "notes: " .. tostring(settings["visualizer"]["notes"]),
+ "stars: " .. tostring(settings["visualizer"]["stars"]),
+ "----- external -----"
+ }
+
+ for i, v in ipairs(externalVisualizers) do
+ if settings["visualizer"][v[1]] == nil then
+ settings["visualizer"][v[1]] = false
+ end
+
+ table.insert(tempSet, v[1] .. ": " .. tostring(settings["visualizer"][v[1]]))
+ end
+
+ settingsList:set(tempSet)
+ end
+ saveSettings()
+end
diff --git a/unused-code/fx.lua b/unused-code/fx.lua
deleted file mode 100644
index ddf8fe1..0000000
--- a/unused-code/fx.lua
+++ /dev/null
@@ -1,201 +0,0 @@
--- unused code for fx support.
-
--- in uiToolkit
-
-fxScreen = {}
-fxScreen.inst = nil
-fxScreen.instLoc = nil
-fxScreen.oldUpdate = nil
-fxScreen.ui = {
- {Knob(30,30,11),Knob(90,30,11),Knob(150,30,11)}, -- bitcrush
- {Knob(30,30,21),Knob(90,30,11)}, -- opf
- {Knob(30,30,5),Knob(90,30,21),Knob(150,30,11),Knob(210,30,11),Knob(270,30,11)}, -- tpf
- {Knob(30,30,11),Knob(90,30,11)}, -- ringmod
- {Knob(30,30,11),Knob(90,30,11),Knob(150,30,11),Knob(210,30,11)}, -- od
- {Knob(30,30,11),Knob(90,30,11),Knob(150,30,11),Knob(210,30,11),Knob(270,30,11)} -- delay
-}
-fxScreen.fx = {snd.bitcrusher.new,snd.onepolefilter.new,snd.twopolefilter.new,snd.ringmod.new,snd.overdrive.new,snd.delayline.new}
-fxScreen.fxNames = {"bitcrusher","one pole filter","two pole filter","ringmod","overdrive","delayline"}
-fxScreen.appliedFx = {}
-fxScreen.appliedFxNames = {}
-fxScreen.appliedFxParams = {}
-fxScreen.edit = 0
-fxScreen.currentElem = 1
-
-function fxScreen.open()
- --fxScreen.inst = inst
- --fxScreen.instLoc = table.find(instrumentTable,inst)
-
- fxList:set(table.join(fxScreen.appliedFxNames, {"add effect +"}))
- fxList:setSelectedRow(1)
-
- pd.inputHandlers.push(fxScreen,true)
- fxScreen.oldUpdate = pd.update
- pd.update = fxScreen.update
-end
-
-function fxScreen.update()
- gfx.clear()
-
- local crank = pd.getCrankTicks(settings["cranksens"])
-
- if crank ~= 0 and fxScreen.edit ~= 0 then
- local elem = fxScreen.currentElem
- local knob = fxScreen.ui[fxScreen.edit][elem]
- local eff = fxScreen.appliedFx[fxScreen.edit]
- local effName = fxScreen.appliedFxNames[fxScreen.edit]
- knob:click(1,crank)
- print(knob:getCurrentClick())
-
- local click = knob:getCurrentClick()
-
- if effName == "bitcrusher" then
- if elem == 1 then
- eff:setAmount((click-1)*0.1)
- elseif elem == 2 then
- eff:setUndersampling((click-1)*0.1)
- else
- eff:setMix((click)*0.1)
- end
- elseif effName == "one pole filter" then
- if elem == 1 then
- eff:setParameter((click-11)*0.1)
- elseif elem == 2 then
- eff:setMix((click)*0.1)
- end
- elseif effName == "two pole filter" then
- local types = {"lowpass","highpass","bandpass","notch","peq","lowshelf","highshelf"}
- if elem == 1 then
- eff:setType(types[click+1])
- elseif elem == 2 then
- eff:setFrequency(click*1000)
- elseif elem == 3 then
- eff:setResonance(click*0.1)
- elseif elem == 4 then
- eff:setGain(click*0.5)
- elseif elem == 5 then
- eff:setMix((click)*0.1)
- end
- elseif effName == "ringmod" then
- if elem == 1 then
- eff:setFrequency(click*5)
- elseif elem == 2 then
- eff:setMix((click)*0.1)
- end
- elseif effName == "overdrive" then
- if elem == 1 then
- eff:setGain(click*2)
- elseif elem == 2 then
- eff:setLimit(click*2)
- elseif elem == 3 then
- eff:setOffset(click*2)
- elseif elem == 4 then
- eff:setMix((click)*0.1)
- end
- end
- end
-
- if fxScreen.edit ~= 0 then
- local sel = false
- for i = 1, #fxScreen.ui[fxScreen.edit], 1 do
- local sel = nil
- if i == fxScreen.currentElem then
- sel = true
- end
- fxScreen.ui[fxScreen.edit][i]:draw(sel)
- end
- else
- fxList:drawInRect(0,0,400,240)
- end
-end
-
-function fxScreen.close()
- pd.inputHandlers.pop()
- pd.update = fxScreen.oldUpdate
-end
-
-function fxScreen.BButtonDown()
- if fxScreen.edit == 0 then
- fxScreen.close()
- else
- fxScreen.edit = 0
- end
-end
-
-function fxScreen.AButtonDown()
- local selRow = fxList:getSelectedRow()
- if fxListContents[selRow] == "add effect +" then
- fxList:set(fxScreen.fxNames)
- fxList:setSelectedRow(1)
- elseif table.find(fxListContents, "add effect +") == -1 then
- local eff
- if fxListContents[selRow] == "delayline" then
- eff = fxScreen.fx[selRow](0.3)
- else
- eff = fxScreen.fx[selRow]()
- end
- table.insert(fxScreen.appliedFxNames,fxScreen.fxNames[selRow])
- --eff:setMix(1)
- --eff:setUndersampling(0.75)
-
- --snd.addEffect(eff)
- snd.addEffect(eff)
- table.insert(fxScreen.appliedFx, eff)
- fxList:set(table.join(fxScreen.appliedFxNames, {"add effect +"}))
- fxList:setSelectedRow(1)
- else
- fxScreen.edit = table.find(fxScreen.fxNames,fxScreen.appliedFxNames[selRow])
- end
-end
-
-function fxScreen.downButtonDown()
- fxList:selectNextRow()
-end
-
-function fxScreen.upButtonDown()
- fxList:selectPreviousRow()
-end
-
-function fxScreen.rightButtonDown()
- if fxListContents[fxList:getSelectedRow()] ~= "add effect +" and fxScreen.edit == 0 then
- -- remove effect name and eff from two lists
- snd.removeEffect(fxScreen.appliedFx[fxList:getSelectedRow()])
- table.remove(fxScreen.appliedFx[fxList:getSelectedRow()])
- table.remove(fxScreen.appliedFxNames[fxList:getSelectedRow()])
- else
- fxScreen.currentElem += 1
- end
-end
-
-function fxScreen.leftButtonDown()
- fxScreen.currentElem -= 1
-end
-
--- lists
-
-fxListContents = {}
-
-fxList = pd.ui.gridview.new(0, 10)
-fxList.backgroundImage = gfx.image.new(10,10,gfx.kColorWhite)
-fxList:setNumberOfRows(1)
-fxList:setCellPadding(0, 0, 5, 10)
-fxList:setContentInset(24, 24, 13, 11)
-
-function fxList:drawCell(section, row, column, selected, x, y, width, height)
- if fxListContents[row] == ".." then
- fxListContents[row] = "Ā"
- end
-
- if selected then
- gfx.fillRoundRect(x, y, width, 20, 4)
- gfx.setImageDrawMode(gfx.kDrawModeNXOR)
- else
- gfx.setImageDrawMode(gfx.kDrawModeNXOR)
- end
- gfx.drawText(fxListContents[row], x+4, y+2, width, height, nil, "...", align.center)
-end
-
-function fxList:set(t)
- fxListContents = t
- fxList:setNumberOfRows(#t)
-end
diff --git a/visualizers/bumper.lua b/visualizers/bumper.lua
new file mode 100644
index 0000000..90caad7
--- /dev/null
+++ b/visualizers/bumper.lua
@@ -0,0 +1,15 @@
+-- bumper visualizer for cs-16 by nanobot567
+--
+-- sets the display offset so the screen bumps to the beat :sunglasses:
+
+local function bumperUpdate(data)
+ if data.beat then
+ pd.display.setOffset(0, 3)
+ elseif data.step % 8 == 2 then
+ pd.display.setOffset(0, 1)
+ else
+ pd.display.setOffset(0, 0)
+ end
+end
+
+return {"bumper", bumperUpdate}
diff --git a/visualizers/lines.lua b/visualizers/lines.lua
new file mode 100644
index 0000000..1d4673f
--- /dev/null
+++ b/visualizers/lines.lua
@@ -0,0 +1,74 @@
+-- lines visualizer for cs-16 by nanobot567
+--
+-- speedlines looking thing
+
+
+class("LineParticle").extends()
+
+function LineParticle:init(x, minlen, maxlen)
+ if x ~= nil then
+ self.x = x
+ else
+ self.x = math.random(1, 400)
+ end
+
+ if minlen then
+ self.minlen = minlen
+ else
+ self.minlen = 4
+ end
+
+ if maxlen then
+ self.maxlen = maxlen
+ else
+ self.maxlen = 12
+ end
+
+ self.y = math.random(20, 220)
+ self.xrestart = 410
+ self.speed = math.random(8, 16)
+ self.length = self.speed + math.random(self.minlen, self.maxlen)
+end
+
+function LineParticle:update(playing)
+ if self.x > self.xrestart and playing then
+ self.x = -20
+ self.y = math.random(20, 220)
+ self.speed = math.random(8, 16)
+ self.length = self.speed + math.random(self.minlen, self.maxlen)
+ else
+ self.x += self.speed
+ end
+
+ gfx.setLineWidth(2)
+
+ if self.x + self.length > 0 then
+ gfx.drawLine(self.x, self.y, self.x + self.length, self.y)
+ end
+ gfx.setLineWidth(1)
+end
+
+function LineParticle:move(x, y)
+ if x then
+ self.x = x
+ end
+
+ if y then
+ self.y = y
+ end
+end
+
+
+local lines = {}
+
+for i = 1, 8 do
+ table.insert(lines, LineParticle(nil, 20, 40))
+end
+
+function linesUpdate(data)
+ for i, v in ipairs(lines) do
+ v:update(data.playing)
+ end
+end
+
+return {"lines", linesUpdate}
diff --git a/visualizers/ramona/images/blink.gif b/visualizers/ramona/images/blink.gif
new file mode 100644
index 0000000..3c4a569
Binary files /dev/null and b/visualizers/ramona/images/blink.gif differ
diff --git a/visualizers/ramona/images/closed-2.gif b/visualizers/ramona/images/closed-2.gif
new file mode 100644
index 0000000..6973c7a
Binary files /dev/null and b/visualizers/ramona/images/closed-2.gif differ
diff --git a/visualizers/ramona/images/closed-old.gif b/visualizers/ramona/images/closed-old.gif
new file mode 100644
index 0000000..196bc71
Binary files /dev/null and b/visualizers/ramona/images/closed-old.gif differ
diff --git a/visualizers/ramona/images/closed.gif b/visualizers/ramona/images/closed.gif
new file mode 100644
index 0000000..f080794
Binary files /dev/null and b/visualizers/ramona/images/closed.gif differ
diff --git a/visualizers/ramona/images/open.gif b/visualizers/ramona/images/open.gif
new file mode 100644
index 0000000..de20fcb
Binary files /dev/null and b/visualizers/ramona/images/open.gif differ
diff --git a/visualizers/ramona/images/talk.gif b/visualizers/ramona/images/talk.gif
new file mode 100644
index 0000000..d4f38f2
Binary files /dev/null and b/visualizers/ramona/images/talk.gif differ
diff --git a/visualizers/ramona/images/twitch.gif b/visualizers/ramona/images/twitch.gif
new file mode 100644
index 0000000..7a6ddd3
Binary files /dev/null and b/visualizers/ramona/images/twitch.gif differ
diff --git a/visualizers/ramona/ramona.lua b/visualizers/ramona/ramona.lua
new file mode 100644
index 0000000..78b0cac
--- /dev/null
+++ b/visualizers/ramona/ramona.lua
@@ -0,0 +1,119 @@
+-- ramona visualizer for cs-16 by nanobot567
+--
+-- fox who bops her head to the beat!
+
+local SCALE = 2 -- set image scale here!
+local TRACK = 0 -- if you would like to have her bop her head to a track's notes, set that here! (set to 0 to revert)
+
+
+local ramona_open = gfx.image.new("visualizers/ramona/images/open"):scaledImage(SCALE)
+local ramona_blink = gfx.image.new("visualizers/ramona/images/blink"):scaledImage(SCALE)
+local ramona_talk = gfx.image.new("visualizers/ramona/images/talk"):scaledImage(SCALE)
+local ramona_twitch = gfx.image.new("visualizers/ramona/images/twitch"):scaledImage(SCALE)
+local ramona_closed = gfx.image.new("visualizers/ramona/images/closed"):scaledImage(SCALE)
+local ramona_closed2 = gfx.image.new("visualizers/ramona/images/closed-2"):scaledImage(SCALE)
+
+local ramonaWidth, ramonaHeight = ramona_open:getSize()
+local drawHeight = 220 - ramonaHeight
+
+local blinkTimer = 0
+local twitchTimer = 0
+local talkTimer = 0
+
+local speechOptions = {
+ "sounding good!",
+ "no, keep playing\nthe song!",
+ "i like this one\na lot.",
+ "bangerrrrr.",
+ "be sure to save,\nhaha.",
+ "thanks for enabling\nme, by the way!",
+ "turn it up!!",
+ "we getting out of\nthe playdate with\nthis one.",
+ "let. them. cook!!",
+ "betcha didn't know\nthere's lore behind\ncs-16...",
+ "hopefully i don't\nsound like a\nbroken record, haha..."
+}
+local speech = ""
+local lastSpeech = "blannnnk"
+local speechHeightAdjustment = 0
+
+-- by default bops head on every quarter note
+
+local animateCheck1 = function (data) -- check if you can play first frame of animation
+ return data.beat
+end
+
+local animateCheck2 = function (data) -- second frame
+ return data.step % 8 == 2 and seq:isPlaying()
+end
+
+if TRACK ~= 0 then
+ animateCheck1 = function(data)
+ if #data.tracks[TRACK]:getNotes(data.sequencer:getCurrentStep(), data.sequencer:getCurrentStep() + 8) > 0 and seq:isPlaying() then
+ return true
+ end
+ return false
+ end
+
+ animateCheck2 = function(data)
+ if #data.tracks[TRACK]:getNotes(data.sequencer:getCurrentStep() - 8, data.sequencer:getCurrentStep()) > 0 and seq:isPlaying() then
+ return true
+ end
+ return false
+ end
+end
+
+
+local function ramonaUpdate(data)
+ gfx.pushContext()
+
+ if data.settings["dark"] then
+ gfx.setColor(gfx.kColorBlack)
+ gfx.fillRect(0, drawHeight, ramonaWidth, ramonaHeight)
+ end
+
+ gfx.setImageDrawMode(gfx.kDrawModeNXOR)
+ if animateCheck1(data) then
+ ramona_closed2:draw(0, drawHeight)
+ elseif animateCheck2(data) then
+ ramona_closed:draw(0, drawHeight)
+ else
+ if seq:isPlaying() then
+ ramona_open:draw(0, drawHeight)
+ blinkTimer, twitchTimer, talkTimer = 0, 0, 0
+ else
+ if twitchTimer == 0 and blinkTimer == 0 and talkTimer == 0 then
+ if math.random(1, 120) == 1 then
+ blinkTimer = 4
+ elseif math.random(1, 210) == 1 then
+ twitchTimer = 2
+ elseif math.random(1, 1000) == 1 then
+ talkTimer = 250
+ speech = speechOptions[math.random(1, #speechOptions)]
+ while speech == lastSpeech do
+ speech = speechOptions[math.random(1, #speechOptions)]
+ end
+ _, speechHeightAdjustment = gfx.getTextSizeForMaxWidth(speech, 400, nil, fnt8x8)
+ speechHeightAdjustment = speechHeightAdjustment / 2
+ end
+ end
+
+ if blinkTimer > 0 then
+ blinkTimer -= 1
+ ramona_blink:draw(0, drawHeight)
+ elseif twitchTimer > 0 then
+ twitchTimer -= 1
+ ramona_twitch:draw(0, drawHeight)
+ elseif talkTimer > 0 then
+ talkTimer -= 1
+ ramona_talk:draw(0, drawHeight)
+ rains1x:drawText(speech, ramonaWidth + 2, (drawHeight + (ramonaHeight / 2) - speechHeightAdjustment))
+ else
+ ramona_open:draw(0, drawHeight)
+ end
+ end
+ end
+ gfx.popContext()
+end
+
+return {"ramona", ramonaUpdate}