Merge branch 'master' into spectrum
This commit is contained in:
commit
a78116ba02
64 changed files with 2700 additions and 640 deletions
|
|
@ -554,6 +554,7 @@ set(ENGINE_SOURCES
|
|||
src/log.cpp
|
||||
src/baseutils.cpp
|
||||
src/fileutils.cpp
|
||||
src/timeutils.cpp
|
||||
src/utfutils.cpp
|
||||
|
||||
extern/itcompress/compression.c
|
||||
|
|
@ -791,6 +792,7 @@ src/engine/export.cpp
|
|||
src/engine/exportDef.cpp
|
||||
src/engine/fileOpsIns.cpp
|
||||
src/engine/fileOpsSample.cpp
|
||||
src/engine/filePlayer.cpp
|
||||
src/engine/filter.cpp
|
||||
src/engine/instrument.cpp
|
||||
src/engine/macroInt.cpp
|
||||
|
|
@ -994,6 +996,7 @@ src/gui/patManager.cpp
|
|||
src/gui/pattern.cpp
|
||||
src/gui/piano.cpp
|
||||
src/gui/presets.cpp
|
||||
src/gui/refPlayer.cpp
|
||||
src/gui/regView.cpp
|
||||
src/gui/sampleEdit.cpp
|
||||
src/gui/scaling.cpp
|
||||
|
|
|
|||
|
|
@ -432,6 +432,7 @@ reserved input portsets:
|
|||
|
||||
reserved output portsets:
|
||||
- `000` through `01F`: chip outputs
|
||||
- `FFC`: reference file/music player (>=238)
|
||||
- `FFD`: wave/sample preview
|
||||
- `FFE`: metronome
|
||||
- `FFF`: "null" portset
|
||||
|
|
|
|||
8
po/de.po
8
po/de.po
|
|
@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
16
po/es.po
16
po/es.po
|
|
@ -4023,20 +4023,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "un sistema diseñado para propósitos de prueba."
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpegio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpegio (x: semitonos; y: semitonos)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Deslizamiento de tono hacia arriba"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Deslizamiento de tono hacia arriba (xx: velocidad)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Deslizamiento de tono hacia abajo"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Deslizamiento de tono hacia abajo (xx: velocidad)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Portamento (xx: velocidad)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
8
po/fi.po
8
po/fi.po
|
|
@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
16
po/fr.po
16
po/fr.po
|
|
@ -3763,20 +3763,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpège"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpège (x: demi-tons; y: demi-tons)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Glissement d'hauteur vers le haut"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Glissement d'hauteur vers le haut (xx: vitesse)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Glissement d'hauteur vers le bas"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Glissement d'hauteur vers le bas (xx: vitesse)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Palier"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Palier (xx: vitesse)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
|
|
@ -3759,19 +3759,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
8
po/hy.po
8
po/hy.po
|
|
@ -3767,19 +3767,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
16
po/id.po
16
po/id.po
|
|
@ -3768,20 +3768,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpegio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpegio (x: banyaknya semitone; y: banyaknya semitone)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Geser nada ke atas"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Geser nada ke atas (xx: kecepatan)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Geser nada ke bawah"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Geser nada ke bawah (xx: kecepatan)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Portamento (xx: kecepatan)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
8
po/ja.po
8
po/ja.po
|
|
@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
16
po/ko.po
16
po/ko.po
|
|
@ -3981,20 +3981,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "테스트 목적으로 설계된 시스템입니다."
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: 아르페지오"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: 아르페지오 (x: 반음 수; y: 반음 수)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: 피치 슬라이드 업"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: 피치 슬라이드 업 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: 피치 슬라이드 다운"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: 피치 슬라이드 다운 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: 포르타멘토"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: 포르타멘토 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
8
po/nl.po
8
po/nl.po
|
|
@ -3806,19 +3806,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
16
po/pl.po
16
po/pl.po
|
|
@ -4091,20 +4091,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "ten system jest przeznaczony do testowania."
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpeggio (x: półtony; y: półtony)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Portamento w górę"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Portamento w górę (xx: szybkość)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Portamento w dół"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Portamento w dół (xx: szybkość)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Auto-portamento (do wskazanej nuty)"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Auto-portamento (do wskazanej nuty; xx: szybkość)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
16
po/pt_BR.po
16
po/pt_BR.po
|
|
@ -4145,20 +4145,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "este é um sistema desenvolvido para propósito de testes."
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpejo"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpejo (x: semitons; y: semitons)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Slide de tom para cima"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Slide de tom para cima (xx: velocidade)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Slide de tom para baixo"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Slide de tom para baixo (xx: velocidade)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Portamento (xx: velocidade)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
14
po/ru.po
14
po/ru.po
|
|
@ -4080,20 +4080,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "это система, разработанная для тестирования."
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Арпеджио"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Портаменто вверх"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Портаменто вверх (xx: скорость)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Портаменто вниз"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Портаменто вниз (xx: скорость)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Авто-портаменто (до указ. ноты)"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Авто-портаменто (до указ. ноты; xx: скорость)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
16
po/sk.po
16
po/sk.po
|
|
@ -3767,20 +3767,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy Arpeggio (x: poltóny; y: poltóny)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx Tónovy šmyk nahor"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx Tónovy šmyk nahor (xx: rýchlosť)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx Tónovy šmyk nadol"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx Tónovy šmyk nadol (xx: rýchlosť)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx Portamento (xx: rýchlosť)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
|
|
@ -3528,20 +3528,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "테스트 목적으로 설계된 시스템입니다."
|
||||
|
||||
#: src/engine/engine.cpp:51
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: 아르페지오"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: 아르페지오 (x: 반음 수; y: 반음 수)"
|
||||
|
||||
#: src/engine/engine.cpp:53
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: 피치 슬라이드 업"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: 피치 슬라이드 업 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:55
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: 피치 슬라이드 다운"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: 피치 슬라이드 다운 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:57
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: 포르타멘토"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: 포르타멘토 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:59
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
|
|
@ -3524,20 +3524,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "이것은 테스트 목적으로 설계된 시스템입니다."
|
||||
|
||||
#: src/engine/engine.cpp:51
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: 아르페지오"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: 아르페지오 (x: 반음; y: 반음)"
|
||||
|
||||
#: src/engine/engine.cpp:53
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: 피치 슬라이드 업"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: 피치 슬라이드 업 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:55
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: 피치 슬라이드 다운"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: 피치 슬라이드 다운 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:57
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: 포르타멘토"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: 포르타멘토 (xx: 속도)"
|
||||
|
||||
#: src/engine/engine.cpp:59
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
|
|
@ -3558,20 +3558,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "este é um sistema desenvolvido para propósito de testes."
|
||||
|
||||
#: src/engine/engine.cpp:51
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpejo"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpejo (x: semitons; y: semitons)"
|
||||
|
||||
#: src/engine/engine.cpp:53
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Slide de tom para cima"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Slide de tom para cima (xx: velocidade)"
|
||||
|
||||
#: src/engine/engine.cpp:55
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Slide de tom para baixo"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Slide de tom para baixo (xx: velocidade)"
|
||||
|
||||
#: src/engine/engine.cpp:57
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Portamento (xx: velocidade)"
|
||||
|
||||
#: src/engine/engine.cpp:59
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
16
po/sv.po
16
po/sv.po
|
|
@ -4025,20 +4025,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "detta är ett system designat för teständamål."
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: Arpeggio (x: halvtoner; y: halvtoner)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: Tonhöjd glider upp"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: Tonhöjd glider upp (xx: hastighet)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: Tonhöjd glider ner"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: Tonhöjd glider ner (xx: hastighet)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: Portamento (xx: hastighet)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
16
po/th.po
16
po/th.po
|
|
@ -3762,20 +3762,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: อาร์เปจโจ"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: อาร์เปจโจ (x: ครึ่งเสียง; y: ครึ่งเสียง)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: เลื่อนระดับเสียงขึ้น"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: เลื่อนระดับเสียงขึ้น (xx: ความเร็ว)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: เลื่อนระดับเสียงลง"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: เลื่อนระดับเสียงลง (xx: ความเร็ว)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: เสียงเลื่อนไหล"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: เสียงเลื่อนไหล (xx: ความเร็ว)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
8
po/tr.po
8
po/tr.po
|
|
@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
8
po/uk.po
8
po/uk.po
|
|
@ -3764,19 +3764,19 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr ""
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
|
|
|
|||
16
po/zh.po
16
po/zh.po
|
|
@ -3798,20 +3798,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "此系统只是为了测试"
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: 琶音"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: 琶音 (x: 半音; y: 半音)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: 音高上滑"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: 音高上滑 (xx: 速率)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: 音高下滑"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: 音高下滑 (xx: 速率)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: 滑音"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: 滑音 (xx: 速率)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
16
po/zh_HK.po
16
po/zh_HK.po
|
|
@ -3798,20 +3798,20 @@ msgid "this is a system designed for testing purposes."
|
|||
msgstr "此系統只是為了測試"
|
||||
|
||||
#: src/engine/engine.cpp:52
|
||||
msgid "00xy: Arpeggio"
|
||||
msgstr "00xy: 琶音"
|
||||
msgid "00xy: Arpeggio (x: semitones; y: semitones)"
|
||||
msgstr "00xy: 琶音 (x: 半音; y: 半音)"
|
||||
|
||||
#: src/engine/engine.cpp:54
|
||||
msgid "01xx: Pitch slide up"
|
||||
msgstr "01xx: 音高上滑"
|
||||
msgid "01xx: Pitch slide up (xx: speed)"
|
||||
msgstr "01xx: 音高上滑 (xx: 速率)"
|
||||
|
||||
#: src/engine/engine.cpp:56
|
||||
msgid "02xx: Pitch slide down"
|
||||
msgstr "02xx: 音高下滑"
|
||||
msgid "02xx: Pitch slide down (xx: speed)"
|
||||
msgstr "02xx: 音高下滑 (xx: 速率)"
|
||||
|
||||
#: src/engine/engine.cpp:58
|
||||
msgid "03xx: Portamento"
|
||||
msgstr "03xx: 滑音"
|
||||
msgid "03xx: Portamento (xx: speed)"
|
||||
msgstr "03xx: 滑音 (xx: 速率)"
|
||||
|
||||
#: src/engine/engine.cpp:60
|
||||
msgid "04xy: Vibrato (x: speed; y: depth)"
|
||||
|
|
|
|||
|
|
@ -1252,10 +1252,9 @@ SafeWriter* DivEngine::saveCommand(DivCSProgress* progress, DivCSOptions options
|
|||
setOrder(0);
|
||||
BUSY_BEGIN_SOFT;
|
||||
// determine loop point
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
walkSong(loopOrder,loopRow,loopEnd);
|
||||
calcSongTimestamps();
|
||||
int loopOrder=curSubSong->ts.loopStart.order;
|
||||
int loopRow=curSubSong->ts.loopStart.row;
|
||||
logI("loop point: %d %d",loopOrder,loopRow);
|
||||
|
||||
int cmdPopularity[256];
|
||||
|
|
|
|||
|
|
@ -52,13 +52,13 @@ void process(void* u, float** in, float** out, int inChans, int outChans, unsign
|
|||
const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNull) {
|
||||
switch (effect) {
|
||||
case 0x00:
|
||||
return _("00xy: Arpeggio");
|
||||
return _("00xy: Arpeggio (x: semitones; y: semitones)");
|
||||
case 0x01:
|
||||
return _("01xx: Pitch slide up");
|
||||
return _("01xx: Pitch slide up (xx: speed)");
|
||||
case 0x02:
|
||||
return _("02xx: Pitch slide down");
|
||||
return _("02xx: Pitch slide down (xx: speed)");
|
||||
case 0x03:
|
||||
return _("03xx: Portamento");
|
||||
return _("03xx: Portamento (xx: speed)");
|
||||
case 0x04:
|
||||
return _("04xy: Vibrato (x: speed; y: depth)");
|
||||
case 0x05:
|
||||
|
|
@ -201,15 +201,9 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul
|
|||
return notNull?_("Invalid effect"):NULL;
|
||||
}
|
||||
|
||||
void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) {
|
||||
void DivEngine::calcSongTimestamps() {
|
||||
if (curSubSong!=NULL) {
|
||||
curSubSong->walk(loopOrder,loopRow,loopEnd,chans,song.jumpTreatment,song.ignoreJumpAtEnd);
|
||||
}
|
||||
}
|
||||
|
||||
void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, int& length) {
|
||||
if (curSubSong!=NULL) {
|
||||
curSubSong->findLength(loopOrder,loopRow,fadeoutLen,rowsForFadeout,hasFFxx,orders,song.grooves,length,chans,song.jumpTreatment,song.ignoreJumpAtEnd);
|
||||
curSubSong->calcTimestamps(chans,song.grooves,song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -566,6 +560,7 @@ void DivEngine::createNew(const char* description, String sysName, bool inBase64
|
|||
BUSY_BEGIN;
|
||||
renderSamples();
|
||||
reset();
|
||||
calcSongTimestamps();
|
||||
BUSY_END;
|
||||
}
|
||||
|
||||
|
|
@ -603,6 +598,7 @@ void DivEngine::createNewFromDefaults() {
|
|||
BUSY_BEGIN;
|
||||
renderSamples();
|
||||
reset();
|
||||
calcSongTimestamps();
|
||||
BUSY_END;
|
||||
}
|
||||
|
||||
|
|
@ -1515,8 +1511,7 @@ String DivEngine::getPlaybackDebugInfo() {
|
|||
"midiTimeDrift: %f\n"
|
||||
"changeOrd: %d\n"
|
||||
"changePos: %d\n"
|
||||
"totalSeconds: %d\n"
|
||||
"totalTicks: %d\n"
|
||||
"totalTime: %s\n"
|
||||
"totalTicksR: %d\n"
|
||||
"curMidiClock: %d\n"
|
||||
"curMidiTime: %d\n"
|
||||
|
|
@ -1528,7 +1523,7 @@ String DivEngine::getPlaybackDebugInfo() {
|
|||
"totalProcessed: %d\n"
|
||||
"bufferPos: %d\n",
|
||||
curOrder,prevOrder,curRow,prevRow,ticks,subticks,totalLoops,lastLoopPos,nextSpeed,divider,cycles,clockDrift,
|
||||
midiClockCycles,midiClockDrift,midiTimeCycles,midiTimeDrift,changeOrd,changePos,totalSeconds,totalTicks,
|
||||
midiClockCycles,midiClockDrift,midiTimeCycles,midiTimeDrift,changeOrd,changePos,totalTime.toString(),
|
||||
totalTicksR,curMidiClock,curMidiTime,totalCmds,lastCmds,cmdsPerSecond,
|
||||
(int)extValue,(int)tempoAccum,(int)totalProcessed,(int)bufferPos
|
||||
);
|
||||
|
|
@ -1648,6 +1643,37 @@ void DivEngine::getCommandStream(std::vector<DivCommand>& where) {
|
|||
BUSY_END;
|
||||
}
|
||||
|
||||
DivFilePlayer* DivEngine::getFilePlayer() {
|
||||
if (curFilePlayer==NULL) {
|
||||
BUSY_BEGIN_SOFT;
|
||||
curFilePlayer=new DivFilePlayer;
|
||||
curFilePlayer->setOutputRate(got.rate);
|
||||
BUSY_END;
|
||||
}
|
||||
return curFilePlayer;
|
||||
}
|
||||
|
||||
bool DivEngine::getFilePlayerSync() {
|
||||
return filePlayerSync;
|
||||
}
|
||||
|
||||
void DivEngine::setFilePlayerSync(bool doSync) {
|
||||
filePlayerSync=doSync;
|
||||
}
|
||||
|
||||
TimeMicros DivEngine::getFilePlayerCue() {
|
||||
return filePlayerCue;
|
||||
}
|
||||
|
||||
void DivEngine::setFilePlayerCue(TimeMicros cue) {
|
||||
filePlayerCue=cue;
|
||||
}
|
||||
|
||||
void DivEngine::syncFilePlayer() {
|
||||
if (curFilePlayer==NULL) return;
|
||||
curFilePlayer->setPosSeconds(totalTime+filePlayerCue);
|
||||
}
|
||||
|
||||
void DivEngine::playSub(bool preserveDrift, int goalRow) {
|
||||
logV("playSub() called");
|
||||
std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now();
|
||||
|
|
@ -1678,10 +1704,10 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) {
|
|||
midiTimeDrift=0;
|
||||
if (!preserveDrift) {
|
||||
ticks=1;
|
||||
subticks=0;
|
||||
tempoAccum=0;
|
||||
totalTicks=0;
|
||||
totalTicksOff=0;
|
||||
totalSeconds=0;
|
||||
totalTime=TimeMicros(0,0);
|
||||
totalTimeDrift=0;
|
||||
totalTicksR=0;
|
||||
curMidiClock=0;
|
||||
curMidiTime=0;
|
||||
|
|
@ -1771,6 +1797,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) {
|
|||
cmdStream.clear();
|
||||
std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now();
|
||||
logV("playSub() took %dµs",std::chrono::duration_cast<std::chrono::microseconds>(timeEnd-timeStart).count());
|
||||
logV("and landed us at %s (%d ticks, %d:%d.%d)",totalTime.toString(),totalTicksR,curOrder,curRow,ticks);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -1998,6 +2025,12 @@ bool DivEngine::play() {
|
|||
output->midiOut->send(TAMidiMessage(TA_MIDI_MACHINE_PLAY,0,0));
|
||||
}
|
||||
bool didItPlay=playing;
|
||||
if (didItPlay) {
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
return didItPlay;
|
||||
}
|
||||
|
|
@ -2014,15 +2047,28 @@ bool DivEngine::playToRow(int row) {
|
|||
keyHit[i]=false;
|
||||
}
|
||||
bool didItPlay=playing;
|
||||
if (didItPlay) {
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
return didItPlay;
|
||||
}
|
||||
|
||||
void DivEngine::stepOne(int row) {
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
curFilePlayer->stop();
|
||||
}
|
||||
|
||||
if (!isPlaying()) {
|
||||
BUSY_BEGIN_SOFT;
|
||||
freelance=false;
|
||||
playSub(false,row);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
}
|
||||
for (int i=0; i<DIV_MAX_CHANS; i++) {
|
||||
keyHit[i]=false;
|
||||
}
|
||||
|
|
@ -2069,6 +2115,10 @@ void DivEngine::stop() {
|
|||
}
|
||||
}
|
||||
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
curFilePlayer->stop();
|
||||
}
|
||||
|
||||
// reset all chan oscs
|
||||
for (int i=0; i<chans; i++) {
|
||||
DivDispatchOscBuffer* buf=disCont[dispatchOfChan[i]].dispatch->getOscBuffer(dispatchChanOfChan[i]);
|
||||
|
|
@ -2507,12 +2557,8 @@ void DivEngine::virtualTempoChanged() {
|
|||
BUSY_END;
|
||||
}
|
||||
|
||||
int DivEngine::getTotalSeconds() {
|
||||
return totalSeconds;
|
||||
}
|
||||
|
||||
int DivEngine::getTotalTicks() {
|
||||
return totalTicks;
|
||||
TimeMicros DivEngine::getCurTime() {
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
bool DivEngine::getRepeatPattern() {
|
||||
|
|
@ -3100,6 +3146,10 @@ void DivEngine::addOrder(int pos, bool duplicate, bool where) {
|
|||
prevOrder=curOrder;
|
||||
if (playing && !freelance) {
|
||||
playSub(false);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
|
|
@ -3152,6 +3202,10 @@ void DivEngine::deepCloneOrder(int pos, bool where) {
|
|||
if (pos<=curOrder) curOrder++;
|
||||
if (playing && !freelance) {
|
||||
playSub(false);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
|
|
@ -3172,6 +3226,10 @@ void DivEngine::deleteOrder(int pos) {
|
|||
if (curOrder>=curSubSong->ordersLen) curOrder=curSubSong->ordersLen-1;
|
||||
if (playing && !freelance) {
|
||||
playSub(false);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
}
|
||||
|
|
@ -3195,6 +3253,10 @@ void DivEngine::moveOrderUp(int& pos) {
|
|||
pos--;
|
||||
if (playing && !freelance) {
|
||||
playSub(false);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
}
|
||||
|
|
@ -3218,6 +3280,10 @@ void DivEngine::moveOrderDown(int& pos) {
|
|||
pos++;
|
||||
if (playing && !freelance) {
|
||||
playSub(false);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
}
|
||||
|
|
@ -3419,6 +3485,12 @@ void DivEngine::autoPatchbay() {
|
|||
}
|
||||
}
|
||||
|
||||
// file player
|
||||
song.patchbay.reserve(DIV_MAX_OUTPUTS);
|
||||
for (unsigned int j=0; j<DIV_MAX_OUTPUTS; j++) {
|
||||
song.patchbay.push_back(0xffc00000|j|(j<<16));
|
||||
}
|
||||
|
||||
// wave/sample preview
|
||||
song.patchbay.reserve(DIV_MAX_OUTPUTS);
|
||||
for (unsigned int j=0; j<DIV_MAX_OUTPUTS; j++) {
|
||||
|
|
@ -3526,6 +3598,73 @@ void DivEngine::noteOff(int chan) {
|
|||
BUSY_END;
|
||||
}
|
||||
|
||||
int DivEngine::getViableChannel(int chan, int off, int ins) {
|
||||
// if the offset is zero, we don't have to do anything
|
||||
if (off==0) return chan;
|
||||
|
||||
// if there isn't an instrument, just offset chan by off
|
||||
if (ins==-1) {
|
||||
return (chan+off)%chans;
|
||||
}
|
||||
|
||||
bool isViable[DIV_MAX_CHANS];
|
||||
bool isAtLeastOneViable=false;
|
||||
int finalChan=chan;
|
||||
int finalChanType=getChannelType(finalChan);
|
||||
|
||||
// this is a copy of the routine in autoNoteOn...... I am lazy
|
||||
DivInstrument* insInst=getIns(ins);
|
||||
for (int i=0; i<chans; i++) {
|
||||
if (ins==-1 || ins>=song.insLen || getPreferInsType(i)==insInst->type || (getPreferInsType(i)==DIV_INS_NULL && finalChanType==DIV_CH_NOISE) || getPreferInsSecondType(i)==insInst->type) {
|
||||
if (insInst->type==DIV_INS_OPL) {
|
||||
if (insInst->fm.ops==2 || getChannelType(i)==DIV_CH_OP) {
|
||||
isViable[i]=true;
|
||||
isAtLeastOneViable=true;
|
||||
} else {
|
||||
isViable[i]=false;
|
||||
}
|
||||
} else {
|
||||
isViable[i]=true;
|
||||
isAtLeastOneViable=true;
|
||||
}
|
||||
} else {
|
||||
isViable[i]=false;
|
||||
}
|
||||
}
|
||||
|
||||
// screw it if none of the channels are viable
|
||||
if (!isAtLeastOneViable) {
|
||||
return (chan+off)%chans;
|
||||
}
|
||||
|
||||
// now offset (confined to viable channels)
|
||||
int channelsCycled=0;
|
||||
int i=(chan+1)%chans;
|
||||
int attempts=0;
|
||||
while (true) {
|
||||
if (isViable[i]) {
|
||||
channelsCycled++;
|
||||
if (channelsCycled==off) {
|
||||
// we found it
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
if (++i>=chans) {
|
||||
i=0;
|
||||
}
|
||||
|
||||
// fail-safe
|
||||
if (++attempts>1024) {
|
||||
logE("getViableChannel(): too many attempts!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// fail-safe
|
||||
return (chan+off)%chans;
|
||||
}
|
||||
|
||||
bool DivEngine::autoNoteOn(int ch, int ins, int note, int vol) {
|
||||
bool isViable[DIV_MAX_CHANS];
|
||||
bool canPlayAnyway=false;
|
||||
|
|
@ -3630,6 +3769,11 @@ void DivEngine::setOrder(unsigned char order) {
|
|||
prevOrder=curOrder;
|
||||
if (playing && !freelance) {
|
||||
playSub(false);
|
||||
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
}
|
||||
BUSY_END;
|
||||
}
|
||||
|
|
@ -3650,6 +3794,10 @@ void DivEngine::updateSysFlags(int system, bool restart, bool render) {
|
|||
if (restart) {
|
||||
if (isPlaying()) {
|
||||
playSub(false);
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
curFilePlayer->play();
|
||||
}
|
||||
} else if (freelance) {
|
||||
reset();
|
||||
}
|
||||
|
|
@ -3898,9 +4046,8 @@ void DivEngine::quitDispatch() {
|
|||
nextSpeed=3;
|
||||
changeOrd=-1;
|
||||
changePos=0;
|
||||
totalTicks=0;
|
||||
totalTicksOff=0;
|
||||
totalSeconds=0;
|
||||
totalTime=TimeMicros(0,0);
|
||||
totalTimeDrift=0;
|
||||
totalTicksR=0;
|
||||
curMidiClock=0;
|
||||
curMidiTime=0;
|
||||
|
|
@ -3909,6 +4056,9 @@ void DivEngine::quitDispatch() {
|
|||
totalCmds=0;
|
||||
lastCmds=0;
|
||||
cmdsPerSecond=0;
|
||||
if (filePlayerSync) {
|
||||
if (curFilePlayer!=NULL) curFilePlayer->stop();
|
||||
}
|
||||
for (int i=0; i<DIV_MAX_CHANS; i++) {
|
||||
isMuted[i]=0;
|
||||
}
|
||||
|
|
@ -4302,6 +4452,10 @@ bool DivEngine::quit(bool saveConfig) {
|
|||
metroBuf=NULL;
|
||||
metroBufLen=0;
|
||||
}
|
||||
if (curFilePlayer!=NULL) {
|
||||
delete curFilePlayer;
|
||||
curFilePlayer=NULL;
|
||||
}
|
||||
if (yrw801ROM!=NULL) delete[] yrw801ROM;
|
||||
if (tg100ROM!=NULL) delete[] tg100ROM;
|
||||
if (mu5ROM!=NULL) delete[] mu5ROM;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
#include "dataErrors.h"
|
||||
#include "safeWriter.h"
|
||||
#include "cmdStream.h"
|
||||
#include "filePlayer.h"
|
||||
#include "../audio/taAudio.h"
|
||||
#include "blip_buf.h"
|
||||
#include <functional>
|
||||
|
|
@ -54,8 +55,8 @@ class DivWorkPool;
|
|||
|
||||
#define DIV_UNSTABLE
|
||||
|
||||
#define DIV_VERSION "dev237"
|
||||
#define DIV_ENGINE_VERSION 237
|
||||
#define DIV_VERSION "dev238"
|
||||
#define DIV_ENGINE_VERSION 238
|
||||
// for imports
|
||||
#define DIV_VERSION_MOD 0xff01
|
||||
#define DIV_VERSION_FC 0xff02
|
||||
|
|
@ -509,8 +510,9 @@ class DivEngine {
|
|||
int midiTimeCycles;
|
||||
double midiTimeDrift;
|
||||
int stepPlay;
|
||||
int changeOrd, changePos, totalSeconds, totalTicks, totalTicksR, curMidiClock, curMidiTime, totalCmds, lastCmds, cmdsPerSecond;
|
||||
double totalTicksOff;
|
||||
int changeOrd, changePos, totalTicksR, curMidiClock, curMidiTime, totalCmds, lastCmds, cmdsPerSecond;
|
||||
TimeMicros totalTime;
|
||||
double totalTimeDrift;
|
||||
int curMidiTimePiece, curMidiTimeCode;
|
||||
unsigned char extValue, pendingMetroTick;
|
||||
DivGroovePattern speeds;
|
||||
|
|
@ -587,6 +589,7 @@ class DivEngine {
|
|||
int samp_temp, samp_prevSample;
|
||||
short* samp_bbIn;
|
||||
short* samp_bbOut;
|
||||
|
||||
unsigned char* metroTick;
|
||||
size_t metroTickLen;
|
||||
float* metroBuf;
|
||||
|
|
@ -596,6 +599,14 @@ class DivEngine {
|
|||
float metroVol;
|
||||
float previewVol;
|
||||
|
||||
float* filePlayerBuf[DIV_MAX_OUTPUTS];
|
||||
size_t filePlayerBufLen;
|
||||
DivFilePlayer* curFilePlayer;
|
||||
bool filePlayerSync;
|
||||
TimeMicros filePlayerCue;
|
||||
int filePlayerLoopTrail;
|
||||
int curFilePlayerTrail;
|
||||
|
||||
size_t totalProcessed;
|
||||
|
||||
unsigned int renderPoolThreads;
|
||||
|
|
@ -738,12 +749,25 @@ class DivEngine {
|
|||
void createNewFromDefaults();
|
||||
// load a file.
|
||||
bool load(unsigned char* f, size_t length, const char* nameHint=NULL);
|
||||
|
||||
// play a binary command stream.
|
||||
bool playStream(unsigned char* f, size_t length);
|
||||
// get the playing stream.
|
||||
DivCSPlayer* getStreamPlayer();
|
||||
// destroy command stream player.
|
||||
bool killStream();
|
||||
|
||||
// get the audio file player.
|
||||
DivFilePlayer* getFilePlayer();
|
||||
// get whether the player is synchronized with song playback.
|
||||
bool getFilePlayerSync();
|
||||
void setFilePlayerSync(bool doSync);
|
||||
// get/set file player cue position.
|
||||
TimeMicros getFilePlayerCue();
|
||||
void setFilePlayerCue(TimeMicros cue);
|
||||
// UNSAFE - sync file player to current playback position.
|
||||
void syncFilePlayer();
|
||||
|
||||
// save as .dmf.
|
||||
SafeWriter* saveDMF(unsigned char version);
|
||||
// save as .fur.
|
||||
|
|
@ -851,11 +875,8 @@ class DivEngine {
|
|||
int convertPanSplitToLinearLR(unsigned char left, unsigned char right, int range);
|
||||
unsigned int convertPanLinearToSplit(int val, unsigned char bits, int range);
|
||||
|
||||
// find song loop position
|
||||
void walkSong(int& loopOrder, int& loopRow, int& loopEnd);
|
||||
|
||||
// find song length in rows (up to specified loop point), and find length of every order
|
||||
void findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, int& length);
|
||||
// calculate all song timestamps
|
||||
void calcSongTimestamps();
|
||||
|
||||
// play (returns whether successful)
|
||||
bool play();
|
||||
|
|
@ -1028,8 +1049,7 @@ class DivEngine {
|
|||
void virtualTempoChanged();
|
||||
|
||||
// get time
|
||||
int getTotalTicks(); // 1/1000000th of a second
|
||||
int getTotalSeconds();
|
||||
TimeMicros getCurTime();
|
||||
|
||||
// get repeat pattern
|
||||
bool getRepeatPattern();
|
||||
|
|
@ -1182,6 +1202,10 @@ class DivEngine {
|
|||
// set whether autoNoteIn is mono or poly
|
||||
void setAutoNotePoly(bool poly);
|
||||
|
||||
// get next viable channel with an offset
|
||||
// chan is the base channel, off is the offset and ins is the instrument.
|
||||
int getViableChannel(int chan, int off, int ins);
|
||||
|
||||
// go to order
|
||||
void setOrder(unsigned char order);
|
||||
|
||||
|
|
@ -1504,15 +1528,13 @@ class DivEngine {
|
|||
stepPlay(0),
|
||||
changeOrd(-1),
|
||||
changePos(0),
|
||||
totalSeconds(0),
|
||||
totalTicks(0),
|
||||
totalTicksR(0),
|
||||
curMidiClock(0),
|
||||
curMidiTime(0),
|
||||
totalCmds(0),
|
||||
lastCmds(0),
|
||||
cmdsPerSecond(0),
|
||||
totalTicksOff(0.0),
|
||||
totalTimeDrift(0.0),
|
||||
curMidiTimePiece(0),
|
||||
curMidiTimeCode(0),
|
||||
extValue(0),
|
||||
|
|
@ -1552,6 +1574,12 @@ class DivEngine {
|
|||
metroAmp(0.0f),
|
||||
metroVol(1.0f),
|
||||
previewVol(1.0f),
|
||||
filePlayerBufLen(0),
|
||||
curFilePlayer(NULL),
|
||||
filePlayerSync(false),
|
||||
filePlayerCue(0,0),
|
||||
filePlayerLoopTrail(0),
|
||||
curFilePlayerTrail(0),
|
||||
totalProcessed(0),
|
||||
renderPoolThreads(0),
|
||||
renderPool(NULL),
|
||||
|
|
@ -1584,6 +1612,7 @@ class DivEngine {
|
|||
memset(oscBuf,0,DIV_MAX_OUTPUTS*(sizeof(float*)));
|
||||
memset(exportChannelMask,1,DIV_MAX_CHANS*sizeof(bool));
|
||||
memset(chipPeak,0,DIV_MAX_CHIPS*DIV_MAX_OUTPUTS*sizeof(float));
|
||||
memset(filePlayerBuf,0,DIV_MAX_OUTPUTS*sizeof(float));
|
||||
|
||||
for (int i=0; i<DIV_MAX_CHIP_DEFS; i++) {
|
||||
sysFileMapFur[i]=DIV_SYSTEM_NULL;
|
||||
|
|
|
|||
|
|
@ -55,10 +55,9 @@ void DivExportAmigaValidation::run() {
|
|||
EXTERN_BUSY_BEGIN_SOFT;
|
||||
|
||||
// determine loop point
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
int loopOrder=e->curSubSong->ts.loopStart.order;
|
||||
int loopRow=e->curSubSong->ts.loopStart.row;
|
||||
|
||||
e->curOrder=0;
|
||||
e->freelance=false;
|
||||
|
|
|
|||
|
|
@ -76,10 +76,9 @@ void DivExportGRUB::run() {
|
|||
e->got.rate=rate;
|
||||
|
||||
// Determine loop point.
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
int loopOrder=e->curSubSong->ts.loopStart.order;
|
||||
int loopRow=e->curSubSong->ts.loopStart.row;
|
||||
logAppendf("loop point: %d %d",loopOrder,loopRow);
|
||||
e->warnings="";
|
||||
|
||||
|
|
|
|||
|
|
@ -75,10 +75,9 @@ void DivExportiPod::run() {
|
|||
e->got.rate=rate;
|
||||
|
||||
// Determine loop point.
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
int loopOrder=e->curSubSong->ts.loopStart.order;
|
||||
int loopRow=e->curSubSong->ts.loopStart.row;
|
||||
logAppendf("loop point: %d %d",loopOrder,loopRow);
|
||||
e->warnings="";
|
||||
|
||||
|
|
|
|||
|
|
@ -92,10 +92,9 @@ void DivExportSAPR::run() {
|
|||
e->got.rate=sapRate;
|
||||
|
||||
// Determine loop point.
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
int loopOrder=e->curSubSong->ts.loopStart.order;
|
||||
int loopRow=e->curSubSong->ts.loopStart.row;
|
||||
logAppendf("loop point: %d %d",loopOrder,loopRow);
|
||||
e->warnings="";
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ static void writeCmd(std::vector<TiunaBytes>& cmds, TiunaCmd& cmd, unsigned char
|
|||
}
|
||||
|
||||
void DivExportTiuna::run() {
|
||||
int loopOrder, loopOrderRow, loopEnd;
|
||||
int loopOrder, loopOrderRow;
|
||||
int tick=0;
|
||||
SafeWriter* w;
|
||||
std::map<int,TiunaCmd> allCmds[2];
|
||||
|
|
@ -199,10 +199,9 @@ void DivExportTiuna::run() {
|
|||
e->synchronizedSoft([&]() {
|
||||
// determine loop point
|
||||
// bool stopped=false;
|
||||
loopOrder=0;
|
||||
loopOrderRow=0;
|
||||
loopEnd=0;
|
||||
e->walkSong(loopOrder,loopOrderRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
loopOrder=e->curSubSong->ts.loopStart.order;
|
||||
loopOrderRow=e->curSubSong->ts.loopStart.row;
|
||||
logAppendf("loop point: %d %d",loopOrder,loopOrderRow);
|
||||
|
||||
w=new SafeWriter;
|
||||
|
|
|
|||
|
|
@ -574,10 +574,9 @@ void DivExportZSM::run() {
|
|||
e->got.rate=zsmrate&0xffff;
|
||||
|
||||
// determine loop point
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
int loopOrder=e->curSubSong->ts.loopStart.order;
|
||||
int loopRow=e->curSubSong->ts.loopStart.row;
|
||||
logAppendf("loop point: %d %d",loopOrder,loopRow);
|
||||
|
||||
zsm.init(zsmrate);
|
||||
|
|
|
|||
556
src/engine/filePlayer.cpp
Normal file
556
src/engine/filePlayer.cpp
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
/**
|
||||
* Furnace Tracker - multi-system chiptune tracker
|
||||
* Copyright (C) 2021-2025 tildearrow and contributors
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "filePlayer.h"
|
||||
#include "filter.h"
|
||||
#include "../ta-log.h"
|
||||
#include <inttypes.h>
|
||||
#include <chrono>
|
||||
|
||||
#define DIV_FPCACHE_BLOCK_SHIFT 15
|
||||
#define DIV_FPCACHE_BLOCK_SIZE (1<<DIV_FPCACHE_BLOCK_SHIFT)
|
||||
#define DIV_FPCACHE_BLOCK_MASK (DIV_FPCACHE_BLOCK_SIZE-1)
|
||||
|
||||
#define DIV_FPCACHE_BLOCKS_FROM_FILL 3
|
||||
#define DIV_FPCACHE_DISCARD_SIZE 4096
|
||||
|
||||
// 5MB should be enough
|
||||
#define DIV_MAX_MEMORY (5<<20)
|
||||
|
||||
#define DIV_NO_BLOCK (-10)
|
||||
|
||||
void DivFilePlayer::fillBlocksNear(ssize_t pos) {
|
||||
logV("DivFilePlayer: fillBlocksNear(%" PRIu64 ")",pos);
|
||||
|
||||
// don't if file isn't present
|
||||
if (!blocks) return;
|
||||
|
||||
// don't if there was an I/O error
|
||||
if (fileError) return;
|
||||
|
||||
// don't read anything if we cannot seek
|
||||
// (if this is set the file is already read in its entirety)
|
||||
if (!si.seekable) return;
|
||||
|
||||
ssize_t firstBlock=pos>>DIV_FPCACHE_BLOCK_SHIFT;
|
||||
ssize_t lastBlock=firstBlock+DIV_FPCACHE_BLOCKS_FROM_FILL;
|
||||
if (firstBlock<0) firstBlock=0;
|
||||
if (firstBlock>=(ssize_t)numBlocks) firstBlock=numBlocks-1;
|
||||
if (lastBlock<0) lastBlock=0;
|
||||
if (lastBlock>=(ssize_t)numBlocks) lastBlock=numBlocks-1;
|
||||
|
||||
// don't read if we're after end of file
|
||||
if (firstBlock>lastBlock) return;
|
||||
|
||||
bool needToFill=false;
|
||||
for (ssize_t i=firstBlock; i<=lastBlock; i++) {
|
||||
if (i<0 || i>=(ssize_t)numBlocks) continue;
|
||||
if (!blocks[i]) {
|
||||
needToFill=true;
|
||||
firstBlock=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needToFill) return;
|
||||
|
||||
// check whether we need to seek
|
||||
sf_count_t curSeek=sf_seek(sf,0,SEEK_CUR);
|
||||
if (curSeek==-1) {
|
||||
// I/O error
|
||||
fileError=true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((curSeek&DIV_FPCACHE_BLOCK_MASK)!=0 || (curSeek>>DIV_FPCACHE_BLOCK_SHIFT)!=firstBlock) {
|
||||
// we need to seek
|
||||
logV("- seeking");
|
||||
// we seek to a previous position in order to compensate for possible decoding differences when seeking
|
||||
// (usually in lossy codecs)
|
||||
sf_count_t seekWhere=firstBlock<<DIV_FPCACHE_BLOCK_SHIFT;
|
||||
if (seekWhere<DIV_FPCACHE_DISCARD_SIZE) {
|
||||
curSeek=sf_seek(sf,0,SEEK_SET);
|
||||
// discard
|
||||
if (sf_readf_float(sf,discardBuf,seekWhere)!=seekWhere) {
|
||||
// this is a problem
|
||||
}
|
||||
} else {
|
||||
seekWhere-=DIV_FPCACHE_DISCARD_SIZE;
|
||||
curSeek=sf_seek(sf,seekWhere,SEEK_SET);
|
||||
// discard
|
||||
if (sf_readf_float(sf,discardBuf,DIV_FPCACHE_DISCARD_SIZE)!=DIV_FPCACHE_DISCARD_SIZE) {
|
||||
// this is a problem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read blocks
|
||||
for (ssize_t i=firstBlock; i<=lastBlock; i++) {
|
||||
if (!blocks[i]) {
|
||||
blocks[i]=new float[DIV_FPCACHE_BLOCK_SIZE*si.channels];
|
||||
memset(blocks[i],0,DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float));
|
||||
}
|
||||
logV("- reading block %" PRIu64,i);
|
||||
sf_count_t totalRead=sf_readf_float(sf,blocks[i],DIV_FPCACHE_BLOCK_SIZE);
|
||||
if (totalRead<DIV_FPCACHE_BLOCK_SIZE) {
|
||||
// we've reached end of file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DivFilePlayer::collectGarbage(ssize_t pos) {
|
||||
// don't if file isn't present
|
||||
if (!blocks) return;
|
||||
|
||||
// don't if there was an I/O error
|
||||
if (fileError) return;
|
||||
|
||||
// don't if we cannot seek
|
||||
if (!si.seekable) return;
|
||||
|
||||
size_t memUsage=getMemUsage();
|
||||
if (memUsage<DIV_MAX_MEMORY) return;
|
||||
|
||||
pos>>=DIV_FPCACHE_BLOCK_SHIFT;
|
||||
if (pos<0) pos=0;
|
||||
if (pos>=(ssize_t)numBlocks) pos=numBlocks-1;
|
||||
|
||||
// collect garbage
|
||||
// start with blocks before the given position
|
||||
// then try with blocks after given position
|
||||
// prioritize far away ones
|
||||
// do not destroy priority blocks
|
||||
for (ssize_t i=0; i<pos-2; i++) {
|
||||
if (!blocks[i]) continue;
|
||||
if (priorityBlock[i]) continue;
|
||||
logV("erasing block %d",(int)i);
|
||||
float* block=blocks[i];
|
||||
blocks[i]=NULL;
|
||||
delete[] block;
|
||||
|
||||
memUsage-=DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float);
|
||||
if (memUsage<DIV_MAX_MEMORY) return;
|
||||
}
|
||||
for (ssize_t i=numBlocks-1; i>pos+DIV_FPCACHE_BLOCKS_FROM_FILL; i--) {
|
||||
if (!blocks[i]) continue;
|
||||
if (priorityBlock[i]) continue;
|
||||
logV("erasing block %d",(int)i);
|
||||
float* block=blocks[i];
|
||||
blocks[i]=NULL;
|
||||
delete[] block;
|
||||
|
||||
memUsage-=DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float);
|
||||
if (memUsage<DIV_MAX_MEMORY) return;
|
||||
}
|
||||
}
|
||||
|
||||
void DivFilePlayer::runCacheThread() {
|
||||
std::unique_lock<std::mutex> lock(cacheThreadLock);
|
||||
|
||||
while (!quitThread) {
|
||||
ssize_t wantBlockC=wantBlock;
|
||||
if (wantBlockC!=DIV_NO_BLOCK) {
|
||||
wantBlock=DIV_NO_BLOCK;
|
||||
logV("thread fill %" PRIu64,wantBlockC);
|
||||
fillBlocksNear(wantBlockC);
|
||||
collectGarbage(wantBlockC);
|
||||
}
|
||||
cacheCV.wait(lock);
|
||||
}
|
||||
|
||||
threadHasQuit=true;
|
||||
logV("DivFilePlayer: cache thread over.");
|
||||
}
|
||||
|
||||
float DivFilePlayer::getSampleAt(ssize_t pos, int ch) {
|
||||
if (blocks==NULL) return 0.0f;
|
||||
ssize_t blockIndex=pos>>DIV_FPCACHE_BLOCK_SHIFT;
|
||||
if (blockIndex<0 || blockIndex>=(ssize_t)numBlocks) return 0.0f;
|
||||
|
||||
float* block=blocks[blockIndex];
|
||||
size_t posInBlock=(pos&DIV_FPCACHE_BLOCK_MASK)*si.channels+ch;
|
||||
if (block==NULL) return 0.0f;
|
||||
|
||||
return block[posInBlock];
|
||||
}
|
||||
|
||||
void DivFilePlayer::mix(float** buf, int chans, unsigned int size) {
|
||||
// fill with zero if we don't have a file
|
||||
if (sf==NULL) {
|
||||
for (int i=0; i<chans; i++) {
|
||||
memset(buf[i],0,size*sizeof(float));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float actualVolume=volume+1.0f;
|
||||
if (actualVolume<0.0f) actualVolume=0.0f;
|
||||
if (actualVolume>1.0f) actualVolume=1.0f;
|
||||
|
||||
if (wantBlock!=DIV_NO_BLOCK) {
|
||||
cacheCV.notify_one();
|
||||
}
|
||||
|
||||
for (unsigned int i=0; i<size; i++) {
|
||||
// acknowledge pending events
|
||||
if (pendingPosOffset==i) {
|
||||
pendingPosOffset=UINT_MAX;
|
||||
playPos=pendingPos;
|
||||
rateAccum=0;
|
||||
}
|
||||
if (pendingPlayOffset==i) {
|
||||
playing=true;
|
||||
}
|
||||
if (pendingStopOffset==i) {
|
||||
playing=false;
|
||||
}
|
||||
|
||||
ssize_t blockIndex=playPos>>DIV_FPCACHE_BLOCK_SHIFT;
|
||||
if (blockIndex!=lastWantBlock) {
|
||||
wantBlock=playPos;
|
||||
cacheCV.notify_one();
|
||||
lastWantBlock=blockIndex;
|
||||
}
|
||||
|
||||
if (playing) {
|
||||
// sinc interpolation
|
||||
float x[8];
|
||||
|
||||
unsigned int n=(8192*rateAccum)/outRate;
|
||||
n&=8191;
|
||||
float* t1=&sincTable[(8191-n)<<2];
|
||||
float* t2=&sincTable[n<<2];
|
||||
if (si.channels==1) {
|
||||
// mono optimization
|
||||
for (int k=0; k<8; k++) {
|
||||
x[k]=getSampleAt(playPos+k-3,0);
|
||||
}
|
||||
|
||||
float s=(
|
||||
x[0]*t2[3]+
|
||||
x[1]*t2[2]+
|
||||
x[2]*t2[1]+
|
||||
x[3]*t2[0]+
|
||||
x[4]*t1[0]+
|
||||
x[5]*t1[1]+
|
||||
x[6]*t1[2]+
|
||||
x[7]*t1[3]
|
||||
)*actualVolume;
|
||||
|
||||
for (int j=0; j<chans; j++) {
|
||||
buf[j][i]=s;
|
||||
}
|
||||
} else for (int j=0; j<chans; j++) {
|
||||
if (j>=si.channels) {
|
||||
buf[j][i]=0.0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int k=0; k<8; k++) {
|
||||
x[k]=getSampleAt(playPos+k-3,j);
|
||||
}
|
||||
buf[j][i]=(
|
||||
x[0]*t2[3]+
|
||||
x[1]*t2[2]+
|
||||
x[2]*t2[1]+
|
||||
x[3]*t2[0]+
|
||||
x[4]*t1[0]+
|
||||
x[5]*t1[1]+
|
||||
x[6]*t1[2]+
|
||||
x[7]*t1[3]
|
||||
)*actualVolume;
|
||||
}
|
||||
|
||||
// advance
|
||||
rateAccum+=si.samplerate;
|
||||
while (rateAccum>=outRate) {
|
||||
rateAccum-=outRate;
|
||||
playPos++;
|
||||
/*if (playPos>=(ssize_t)si.frames) {
|
||||
playPos=0;
|
||||
}*/
|
||||
}
|
||||
} else {
|
||||
for (int j=0; j<chans; j++) {
|
||||
buf[j][i]=0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t DivFilePlayer::getPos() {
|
||||
return playPos;
|
||||
}
|
||||
|
||||
TimeMicros DivFilePlayer::getPosSeconds() {
|
||||
if (sf==NULL) {
|
||||
return TimeMicros(0,0);
|
||||
}
|
||||
double microsD=playPos%si.samplerate;
|
||||
return TimeMicros(
|
||||
playPos/si.samplerate, // seconds
|
||||
(int)((1000000.0*microsD)/(double)si.samplerate) // microseconds
|
||||
);
|
||||
}
|
||||
|
||||
ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) {
|
||||
if (offset==UINT_MAX) {
|
||||
playPos=newPos;
|
||||
rateAccum=0;
|
||||
wantBlock=playPos;
|
||||
logD("DivFilePlayer: setPos(%" PRIi64 ")",newPos);
|
||||
return playPos;
|
||||
} else {
|
||||
pendingPosOffset=offset;
|
||||
pendingPos=newPos;
|
||||
wantBlock=playPos;
|
||||
logD("DivFilePlayer: offset %u setPos(%" PRIi64 ")",offset,newPos);
|
||||
return newPos;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t DivFilePlayer::setPosSeconds(TimeMicros newTime, unsigned int offset) {
|
||||
if (sf==NULL) return 0;
|
||||
double microsD=(double)si.samplerate*((double)newTime.micros/1000000.0);
|
||||
if (offset==UINT_MAX) {
|
||||
playPos=(ssize_t)newTime.seconds*(ssize_t)si.samplerate+(int)microsD;
|
||||
rateAccum=0;
|
||||
wantBlock=playPos;
|
||||
logD("DivFilePlayer: setPosSeconds(%s)",newTime.toString());
|
||||
return playPos;
|
||||
} else {
|
||||
pendingPosOffset=offset;
|
||||
pendingPos=(ssize_t)newTime.seconds*(ssize_t)si.samplerate+(int)microsD;
|
||||
wantBlock=pendingPos;
|
||||
logD("DivFilePlayer: offset %u setPosSeconds(%s)",offset,newTime.toString());
|
||||
return pendingPos;
|
||||
}
|
||||
}
|
||||
|
||||
size_t DivFilePlayer::getMemUsage() {
|
||||
if (blocks==NULL) return 0;
|
||||
size_t ret=0;
|
||||
for (size_t i=0; i<numBlocks; i++) {
|
||||
if (blocks[i]) ret+=DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool DivFilePlayer::isBlockPresent(ssize_t pos) {
|
||||
if (blocks==NULL) return false;
|
||||
ssize_t which=pos>>DIV_FPCACHE_BLOCK_SHIFT;
|
||||
if (which<0 || which>=(ssize_t)numBlocks) return false;
|
||||
return (blocks[which]!=NULL);
|
||||
}
|
||||
|
||||
bool DivFilePlayer::setBlockPriority(ssize_t pos, bool priority) {
|
||||
if (priorityBlock==NULL) return false;
|
||||
ssize_t which=pos>>DIV_FPCACHE_BLOCK_SHIFT;
|
||||
if (which<0 || which>=(ssize_t)numBlocks) return false;
|
||||
priorityBlock[which]=priority;
|
||||
return priority;
|
||||
}
|
||||
|
||||
bool DivFilePlayer::isLoaded() {
|
||||
return (sf!=NULL);
|
||||
}
|
||||
|
||||
bool DivFilePlayer::isPlaying() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
void DivFilePlayer::play(unsigned int offset) {
|
||||
if (offset!=UINT_MAX) {
|
||||
pendingPlayOffset=offset;
|
||||
logV("DivFilePlayer: playing (offset: %u)",offset);
|
||||
} else {
|
||||
playing=true;
|
||||
logV("DivFilePlayer: playing");
|
||||
}
|
||||
}
|
||||
|
||||
void DivFilePlayer::stop(unsigned int offset) {
|
||||
if (offset!=UINT_MAX) {
|
||||
pendingStopOffset=offset;
|
||||
logV("DivFilePlayer: stopping (offset: %u)",offset);
|
||||
} else {
|
||||
playing=false;
|
||||
logV("DivFilePlayer: stopping");
|
||||
}
|
||||
}
|
||||
|
||||
bool DivFilePlayer::closeFile() {
|
||||
if (sf==NULL) return false;
|
||||
|
||||
logD("DivFilePlayer: closing file.");
|
||||
|
||||
if (cacheThread) {
|
||||
quitThread=true;
|
||||
while (!threadHasQuit) {
|
||||
cacheCV.notify_one();
|
||||
std::this_thread::yield();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
// this join is guaranteed to work
|
||||
cacheThread->join();
|
||||
delete cacheThread;
|
||||
cacheThread=NULL;
|
||||
}
|
||||
|
||||
sfw.doClose();
|
||||
sf=NULL;
|
||||
playing=false;
|
||||
quitThread=false;
|
||||
threadHasQuit=false;
|
||||
|
||||
for (size_t i=0; i<numBlocks; i++) {
|
||||
if (blocks[i]) {
|
||||
delete[] blocks[i];
|
||||
blocks[i]=NULL;
|
||||
}
|
||||
}
|
||||
numBlocks=0;
|
||||
delete[] blocks;
|
||||
blocks=NULL;
|
||||
|
||||
delete[] discardBuf;
|
||||
discardBuf=NULL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DivFilePlayer::loadFile(const char* path) {
|
||||
if (sf!=NULL) closeFile();
|
||||
|
||||
logD("DivFilePlayer: opening file...");
|
||||
sf=sfw.doOpen(path,SFM_READ,&si);
|
||||
if (sf==NULL) {
|
||||
logE("could not open file!");
|
||||
return false;
|
||||
}
|
||||
|
||||
logV("- samples: %d",si.frames);
|
||||
logV("- channels: %d",si.channels);
|
||||
logV("- rate: %d",si.samplerate);
|
||||
|
||||
numBlocks=(DIV_FPCACHE_BLOCK_MASK+si.frames)>>DIV_FPCACHE_BLOCK_SHIFT;
|
||||
blocks=new float*[numBlocks];
|
||||
priorityBlock=new bool[numBlocks];
|
||||
memset(blocks,0,numBlocks*sizeof(void*));
|
||||
memset(priorityBlock,0,numBlocks*sizeof(bool));
|
||||
|
||||
// mark the first blocks as important
|
||||
for (size_t i=0; i<DIV_FPCACHE_BLOCKS_FROM_FILL; i++) {
|
||||
if (i>=numBlocks) break;
|
||||
priorityBlock[i]=true;
|
||||
}
|
||||
|
||||
playPos=0;
|
||||
lastWantBlock=DIV_NO_BLOCK;
|
||||
rateAccum=0;
|
||||
fileError=false;
|
||||
|
||||
// read the entire file if not seekable
|
||||
if (!si.seekable) {
|
||||
logV("file not seekable - reading...");
|
||||
for (size_t i=0; i<numBlocks; i++) {
|
||||
blocks[i]=new float[DIV_FPCACHE_BLOCK_SIZE*si.channels];
|
||||
}
|
||||
for (size_t i=0; i<numBlocks; i++) {
|
||||
sf_count_t totalRead=sf_readf_float(sf,blocks[i],DIV_FPCACHE_BLOCK_SIZE);
|
||||
if (totalRead<DIV_FPCACHE_BLOCK_SIZE) {
|
||||
// we've reached end of file
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logV("file is seekable");
|
||||
// read the first couple blocks
|
||||
fillBlocksNear(0);
|
||||
}
|
||||
|
||||
discardBuf=new float[DIV_FPCACHE_DISCARD_SIZE*si.channels];
|
||||
|
||||
// stsrt the block cache thread
|
||||
if (cacheThread==NULL) {
|
||||
quitThread=false;
|
||||
threadHasQuit=false;
|
||||
cacheThread=new std::thread(&DivFilePlayer::runCacheThread,this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String DivFilePlayer::getLastError() {
|
||||
return lastError;
|
||||
}
|
||||
|
||||
const SF_INFO& DivFilePlayer::getFileInfo() {
|
||||
return si;
|
||||
}
|
||||
|
||||
void DivFilePlayer::setOutputRate(int rate) {
|
||||
if (rate<1) return;
|
||||
outRate=rate;
|
||||
}
|
||||
|
||||
float DivFilePlayer::getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
void DivFilePlayer::setVolume(float vol) {
|
||||
volume=vol;
|
||||
}
|
||||
|
||||
bool DivFilePlayer::getActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
void DivFilePlayer::setActive(bool active) {
|
||||
isActive=active;
|
||||
}
|
||||
|
||||
DivFilePlayer::DivFilePlayer():
|
||||
discardBuf(NULL),
|
||||
blocks(NULL),
|
||||
priorityBlock(NULL),
|
||||
numBlocks(0),
|
||||
sf(NULL),
|
||||
playPos(0),
|
||||
lastWantBlock(DIV_NO_BLOCK),
|
||||
wantBlock(DIV_NO_BLOCK),
|
||||
outRate(44100),
|
||||
rateAccum(0),
|
||||
volume(0.0f),
|
||||
playing(false),
|
||||
fileError(false),
|
||||
quitThread(false),
|
||||
threadHasQuit(false),
|
||||
isActive(false),
|
||||
pendingPos(0),
|
||||
pendingPosOffset(UINT_MAX),
|
||||
pendingPlayOffset(UINT_MAX),
|
||||
pendingStopOffset(UINT_MAX),
|
||||
cacheThread(NULL) {
|
||||
memset(&si,0,sizeof(SF_INFO));
|
||||
sincTable=DivFilterTables::getSincTable8();
|
||||
}
|
||||
|
||||
DivFilePlayer::~DivFilePlayer() {
|
||||
closeFile();
|
||||
}
|
||||
107
src/engine/filePlayer.h
Normal file
107
src/engine/filePlayer.h
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Furnace Tracker - multi-system chiptune tracker
|
||||
* Copyright (C) 2021-2025 tildearrow and contributors
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef _FILEPLAYER_H
|
||||
#define _FILEPLAYER_H
|
||||
|
||||
#include "../ta-utils.h"
|
||||
#include "../timeutils.h"
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
#ifdef HAVE_SNDFILE
|
||||
#include "sfWrapper.h"
|
||||
#else
|
||||
typedef void SNDFILE;
|
||||
struct SF_INFO {
|
||||
int invalid;
|
||||
};
|
||||
#endif
|
||||
|
||||
class DivFilePlayer {
|
||||
float* sincTable;
|
||||
float* discardBuf;
|
||||
float** blocks;
|
||||
bool* priorityBlock;
|
||||
size_t numBlocks;
|
||||
String lastError;
|
||||
SFWrapper sfw;
|
||||
SNDFILE* sf;
|
||||
SF_INFO si;
|
||||
|
||||
ssize_t playPos;
|
||||
ssize_t lastWantBlock;
|
||||
ssize_t wantBlock;
|
||||
int outRate;
|
||||
int rateAccum;
|
||||
float volume;
|
||||
bool playing;
|
||||
bool fileError;
|
||||
bool quitThread;
|
||||
bool threadHasQuit;
|
||||
bool isActive;
|
||||
|
||||
ssize_t pendingPos;
|
||||
unsigned int pendingPosOffset;
|
||||
unsigned int pendingPlayOffset;
|
||||
unsigned int pendingStopOffset;
|
||||
|
||||
std::thread* cacheThread;
|
||||
std::mutex cacheMutex;
|
||||
std::mutex cacheThreadLock;
|
||||
std::condition_variable cacheCV;
|
||||
|
||||
void fillBlocksNear(ssize_t pos);
|
||||
void collectGarbage(ssize_t pos);
|
||||
float getSampleAt(ssize_t pos, int ch);
|
||||
|
||||
public:
|
||||
void runCacheThread();
|
||||
|
||||
size_t getMemUsage();
|
||||
|
||||
void mix(float** buf, int chans, unsigned int size);
|
||||
ssize_t getPos();
|
||||
TimeMicros getPosSeconds();
|
||||
ssize_t setPos(ssize_t newPos, unsigned int offset=UINT_MAX);
|
||||
ssize_t setPosSeconds(TimeMicros newTime, unsigned int offset=UINT_MAX);
|
||||
|
||||
bool isBlockPresent(ssize_t pos);
|
||||
bool setBlockPriority(ssize_t pos, bool priority);
|
||||
bool isLoaded();
|
||||
bool isPlaying();
|
||||
void play(unsigned int offset=UINT_MAX);
|
||||
void stop(unsigned int offset=UINT_MAX);
|
||||
bool closeFile();
|
||||
bool loadFile(const char* path);
|
||||
|
||||
String getLastError();
|
||||
const SF_INFO& getFileInfo();
|
||||
void setOutputRate(int rate);
|
||||
float getVolume();
|
||||
void setVolume(float vol);
|
||||
bool getActive();
|
||||
void setActive(bool active);
|
||||
|
||||
DivFilePlayer();
|
||||
~DivFilePlayer();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -128,4 +128,4 @@ float* DivFilterTables::getSincIntegralSmallTable() {
|
|||
}
|
||||
}
|
||||
return sincIntegralSmallTable;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2169,6 +2169,15 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) {
|
|||
prevOrder=curOrder;
|
||||
prevRow=curRow;
|
||||
playPosLock.unlock();
|
||||
|
||||
// also set the playback position and sync file player if necessary
|
||||
TimeMicros rowTS=curSubSong->ts.getTimes(curOrder,curRow);
|
||||
if (rowTS.seconds!=-1) {
|
||||
totalTime=rowTS;
|
||||
}
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
syncFilePlayer();
|
||||
}
|
||||
}
|
||||
// ...and now process the next row!
|
||||
nextRow();
|
||||
|
|
@ -2572,30 +2581,28 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) {
|
|||
if (stepPlay!=1) {
|
||||
if (!noAccum) {
|
||||
double dt=divider*tickMult;
|
||||
// TODO: is this responsible for timing differences when skipping?
|
||||
if (skipping) {
|
||||
dt*=(double)virtualTempoN/(double)MAX(1,virtualTempoD);
|
||||
}
|
||||
totalTicksR++;
|
||||
// despite the name, totalTicks is in microseconds...
|
||||
totalTicks+=1000000/dt;
|
||||
totalTicksOff+=fmod(1000000.0,dt);
|
||||
while (totalTicksOff>=dt) {
|
||||
totalTicksOff-=dt;
|
||||
totalTicks++;
|
||||
totalTime.micros+=1000000/dt;
|
||||
totalTimeDrift+=fmod(1000000.0,dt);
|
||||
while (totalTimeDrift>=dt) {
|
||||
totalTimeDrift-=dt;
|
||||
totalTime.micros++;
|
||||
}
|
||||
}
|
||||
if (totalTicks>=1000000) {
|
||||
totalTicks-=1000000;
|
||||
if (totalTime.micros>=1000000) {
|
||||
totalTime.micros-=1000000;
|
||||
// who's gonna play a song for 68 years?
|
||||
if (totalSeconds<0x7fffffff) totalSeconds++;
|
||||
if (totalTime.seconds<0x7fffffff) totalTime.seconds++;
|
||||
cmdsPerSecond=totalCmds-lastCmds;
|
||||
lastCmds=totalCmds;
|
||||
}
|
||||
}
|
||||
|
||||
// print status in console mode
|
||||
if (consoleMode && !disableStatusOut && subticks<=1 && !skipping) fprintf(stderr,"\x1b[2K> %d:%.2d:%.2d.%.2d %.2x/%.2x:%.3d/%.3d %4dcmd/s\x1b[G",totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000,curOrder,curSubSong->ordersLen,curRow,curSubSong->patLen,cmdsPerSecond);
|
||||
if (consoleMode && !disableStatusOut && subticks<=1 && !skipping) {
|
||||
String timeFormatted=totalTime.toString(2,TA_TIME_FORMAT_HMS);
|
||||
fprintf(stderr,"\x1b[2K> %s %.2x/%.2x:%.3d/%.3d %4dcmd/s\x1b[G",timeFormatted.c_str(),curOrder,curSubSong->ordersLen,curRow,curSubSong->patLen,cmdsPerSecond);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -3102,6 +3109,19 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
|
|||
// used by audio export to determine how many samples to write (otherwise it'll add silence at the end)
|
||||
lastLoopPos=size-runLeftG;
|
||||
logD("last loop pos: %d for a size of %d and runLeftG of %d",lastLoopPos,size,runLeftG);
|
||||
// if file player is synchronized then set its position to that of the loop row
|
||||
if (curFilePlayer && filePlayerSync) {
|
||||
if (curFilePlayer->isPlaying()) {
|
||||
TimeMicros rowTS=curSubSong->ts.loopStartTime;
|
||||
|
||||
if (rowTS.seconds==-1) {
|
||||
logW("that row isn't supposed to play. report this now!");
|
||||
}
|
||||
|
||||
curFilePlayer->setPosSeconds(rowTS+filePlayerCue,lastLoopPos);
|
||||
}
|
||||
}
|
||||
// increase total loop count
|
||||
totalLoops++;
|
||||
// stop playing once we hit a specific number of loops (set during audio export)
|
||||
if (remainingLoops>0) {
|
||||
|
|
@ -3212,7 +3232,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
|
|||
// complain and stop playback if we believe the engine has stalled
|
||||
//logD("attempts: %d",attempts);
|
||||
if (attempts>=(int)(size+10)) {
|
||||
logE("hang detected! stopping! at %d seconds %d micro (%d>=%d)",totalSeconds,totalTicks,attempts,(int)size);
|
||||
logE("hang detected! stopping! at %s (%d>=%d)",totalTime.toString(),attempts,(int)size);
|
||||
freelance=false;
|
||||
playing=false;
|
||||
extValuePresent=false;
|
||||
|
|
@ -3237,6 +3257,23 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
|
|||
renderPool->wait();
|
||||
}
|
||||
|
||||
// process file player
|
||||
// resize file player audio buffer if necessary
|
||||
if (filePlayerBufLen<size) {
|
||||
for (int i=0; i<DIV_MAX_OUTPUTS; i++) {
|
||||
if (filePlayerBuf[i]!=NULL) delete[] filePlayerBuf[i];
|
||||
filePlayerBuf[i]=new float[size];
|
||||
}
|
||||
filePlayerBufLen=size;
|
||||
}
|
||||
if (curFilePlayer!=NULL && !exporting) {
|
||||
curFilePlayer->mix(filePlayerBuf,outChans,size);
|
||||
} else {
|
||||
for (int i=0; i<DIV_MAX_OUTPUTS; i++) {
|
||||
memset(filePlayerBuf[i],0,size*sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
// process metronome
|
||||
// resize the metronome's audio buffer if necessary
|
||||
if (metroBufLen<size || metroBuf==NULL) {
|
||||
|
|
@ -3274,6 +3311,19 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
|
|||
}
|
||||
}
|
||||
|
||||
// calculate volume of reference file player (so we can attenuate the rest according to the mix slider)
|
||||
// -1 to 0: player volume goes from 0% to 100%
|
||||
// 0 to +1: tracker volume goes from 100% to 0%
|
||||
float refPlayerVol=1.0f;
|
||||
if (curFilePlayer!=NULL) {
|
||||
// only if the player window is open
|
||||
if (curFilePlayer->getActive()) {
|
||||
refPlayerVol=1.0f-curFilePlayer->getVolume();
|
||||
if (refPlayerVol<0.0f) refPlayerVol=0.0f;
|
||||
if (refPlayerVol>1.0f) refPlayerVol=1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// now mix everything (resolve patchbay)
|
||||
for (unsigned int i: song.patchbay) {
|
||||
// there are 4096 portsets. each portset may have up to 16 outputs (subports).
|
||||
|
|
@ -3295,7 +3345,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
|
|||
// chip outputs
|
||||
if (srcPortSet<song.systemLen && playing && !halted) {
|
||||
if (srcSubPort<disCont[srcPortSet].dispatch->getOutputCount()) {
|
||||
float vol=song.systemVol[srcPortSet]*disCont[srcPortSet].dispatch->getPostAmp()*song.masterVol;
|
||||
float vol=song.systemVol[srcPortSet]*disCont[srcPortSet].dispatch->getPostAmp()*song.masterVol*refPlayerVol;
|
||||
|
||||
// apply volume and panning
|
||||
switch (destSubPort&3) {
|
||||
|
|
@ -3317,6 +3367,11 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
|
|||
out[destSubPort][j]+=((float)disCont[srcPortSet].bbOut[srcSubPort][j]/32768.0)*vol;
|
||||
}
|
||||
}
|
||||
} else if (srcPortSet==0xffc) {
|
||||
// file player
|
||||
for (size_t j=0; j<size; j++) {
|
||||
out[destSubPort][j]+=filePlayerBuf[srcSubPort][j];
|
||||
}
|
||||
} else if (srcPortSet==0xffd) {
|
||||
// sample preview
|
||||
for (size_t j=0; j<size; j++) {
|
||||
|
|
|
|||
|
|
@ -19,254 +19,412 @@
|
|||
|
||||
#include "song.h"
|
||||
#include "../ta-log.h"
|
||||
#include <chrono>
|
||||
|
||||
TimeMicros DivSongTimestamps::getTimes(int order, int row) {
|
||||
if (order<0 || order>=DIV_MAX_PATTERNS) return TimeMicros(-1,0);
|
||||
if (row<0 || row>=DIV_MAX_ROWS) return TimeMicros(-1,0);
|
||||
TimeMicros* t=orders[order];
|
||||
if (t==NULL) return TimeMicros(-1,0);
|
||||
return t[row];
|
||||
}
|
||||
|
||||
bool DivSubSong::walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat) {
|
||||
loopOrder=0;
|
||||
loopRow=0;
|
||||
loopEnd=-1;
|
||||
int nextOrder=-1;
|
||||
int nextRow=0;
|
||||
int effectVal=0;
|
||||
int lastSuspectedLoopEnd=-1;
|
||||
DivPattern* subPat[DIV_MAX_CHANS];
|
||||
DivSongTimestamps::DivSongTimestamps():
|
||||
totalTime(0,0),
|
||||
totalTicks(0),
|
||||
totalRows(0),
|
||||
isLoopDefined(false),
|
||||
isLoopable(true) {
|
||||
memset(orders,0,DIV_MAX_PATTERNS*sizeof(void*));
|
||||
memset(maxRow,0,DIV_MAX_PATTERNS);
|
||||
}
|
||||
|
||||
DivSongTimestamps::~DivSongTimestamps() {
|
||||
for (int i=0; i<DIV_MAX_PATTERNS; i++) {
|
||||
if (orders[i]) {
|
||||
delete[] orders[i];
|
||||
orders[i]=NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DivSubSong::calcTimestamps(int chans, std::vector<DivGroovePattern>& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat) {
|
||||
// reduced version of the playback routine for calculation.
|
||||
std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now();
|
||||
|
||||
// reset state
|
||||
ts.totalTime=TimeMicros(0,0);
|
||||
ts.totalTicks=0;
|
||||
ts.totalRows=0;
|
||||
ts.isLoopDefined=true;
|
||||
ts.isLoopable=true;
|
||||
|
||||
memset(ts.maxRow,0,DIV_MAX_PATTERNS);
|
||||
|
||||
for (int i=0; i<DIV_MAX_PATTERNS; i++) {
|
||||
if (ts.orders[i]) {
|
||||
delete[] ts.orders[i];
|
||||
ts.orders[i]=NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// walking state
|
||||
unsigned char wsWalked[8192];
|
||||
memset(wsWalked,0,8192);
|
||||
if (firstPat>0) {
|
||||
memset(wsWalked,255,32*firstPat);
|
||||
}
|
||||
for (int i=firstPat; i<ordersLen; i++) {
|
||||
for (int j=0; j<chans; j++) {
|
||||
subPat[j]=pat[j].getPattern(orders.ord[j][i],false);
|
||||
}
|
||||
if (i>lastSuspectedLoopEnd) {
|
||||
lastSuspectedLoopEnd=i;
|
||||
}
|
||||
for (int j=nextRow; j<patLen; j++) {
|
||||
nextRow=0;
|
||||
bool changingOrder=false;
|
||||
bool jumpingOrder=false;
|
||||
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) {
|
||||
loopOrder=i;
|
||||
loopRow=j;
|
||||
loopEnd=lastSuspectedLoopEnd;
|
||||
return true;
|
||||
}
|
||||
for (int k=0; k<chans; k++) {
|
||||
for (int l=0; l<pat[k].effectCols; l++) {
|
||||
effectVal=subPat[k]->newData[j][DIV_PAT_FXVAL(l)];
|
||||
if (effectVal<0) effectVal=0;
|
||||
if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0x0d) {
|
||||
int curOrder=firstPat;
|
||||
int curRow=0;
|
||||
int prevOrder=firstPat;
|
||||
int prevRow=0;
|
||||
DivGroovePattern curSpeeds=speeds;
|
||||
int curVirtualTempoN=virtualTempoN;
|
||||
int curVirtualTempoD=virtualTempoD;
|
||||
int nextSpeed=curSpeeds.val[0];
|
||||
double divider=hz;
|
||||
double totalMicrosOff=0.0;
|
||||
int ticks=1;
|
||||
int tempoAccum=0;
|
||||
int curSpeed=0;
|
||||
int changeOrd=-1;
|
||||
int changePos=0;
|
||||
unsigned char rowDelay[DIV_MAX_CHANS];
|
||||
unsigned char delayOrder[DIV_MAX_CHANS];
|
||||
unsigned char delayRow[DIV_MAX_CHANS];
|
||||
bool shallStopSched=false;
|
||||
bool shallStop=false;
|
||||
bool songWillEnd=false;
|
||||
bool endOfSong=false;
|
||||
bool rowChanged=false;
|
||||
|
||||
memset(rowDelay,0,DIV_MAX_CHANS);
|
||||
memset(delayOrder,0,DIV_MAX_CHANS);
|
||||
memset(delayRow,0,DIV_MAX_CHANS);
|
||||
if (divider<1) divider=1;
|
||||
|
||||
auto tinyProcessRow=[&,this](int i, bool afterDelay) {
|
||||
// if this is after delay, use the order/row where delay occurred
|
||||
int whatOrder=afterDelay?delayOrder[i]:curOrder;
|
||||
int whatRow=afterDelay?delayRow[i]:curRow;
|
||||
DivPattern* p=pat[i].getPattern(orders.ord[i][whatOrder],false);
|
||||
// pre effects
|
||||
if (!afterDelay) {
|
||||
// set to true if we found an EDxx effect
|
||||
bool returnAfterPre=false;
|
||||
// check all effects
|
||||
for (int j=0; j<pat[i].effectCols; j++) {
|
||||
short effect=p->newData[whatRow][DIV_PAT_FX(j)];
|
||||
short effectVal=p->newData[whatRow][DIV_PAT_FXVAL(j)];
|
||||
|
||||
// empty effect value is the same as zero
|
||||
if (effectVal==-1) effectVal=0;
|
||||
effectVal&=255;
|
||||
|
||||
switch (effect) {
|
||||
case 0x09: // select groove pattern/speed 1
|
||||
if (grooves.empty()) {
|
||||
// special case: sets speed 1 if the song lacks groove patterns
|
||||
if (effectVal>0) curSpeeds.val[0]=effectVal;
|
||||
} else {
|
||||
// sets the groove pattern and resets current speed index
|
||||
if (effectVal<(short)grooves.size()) {
|
||||
curSpeeds=grooves[effectVal];
|
||||
curSpeed=0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x0f: // speed 1/speed 2
|
||||
// if the value is 0 then ignore it
|
||||
if (curSpeeds.len==2 && grooves.empty()) {
|
||||
// if there are two speeds and no groove patterns, set the second speed
|
||||
if (effectVal>0) curSpeeds.val[1]=effectVal;
|
||||
} else {
|
||||
// otherwise set the first speed
|
||||
if (effectVal>0) curSpeeds.val[0]=effectVal;
|
||||
}
|
||||
break;
|
||||
case 0xfd: // virtual tempo num
|
||||
if (effectVal>0) curVirtualTempoN=effectVal;
|
||||
break;
|
||||
case 0xfe: // virtual tempo den
|
||||
if (effectVal>0) curVirtualTempoD=effectVal;
|
||||
break;
|
||||
case 0x0b: // change order
|
||||
// this actually schedules an order change
|
||||
// we perform this change at the end of nextRow()
|
||||
|
||||
// COMPAT FLAG: simultaneous jump treatment
|
||||
if (changeOrd==-1 || jumpTreatment==0) {
|
||||
changeOrd=effectVal;
|
||||
if (jumpTreatment==1 || jumpTreatment==2) {
|
||||
changePos=0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x0d: // next order
|
||||
// COMPAT FLAG: simultaneous jump treatment
|
||||
if (jumpTreatment==2) {
|
||||
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
|
||||
nextOrder=i+1;
|
||||
nextRow=effectVal;
|
||||
jumpingOrder=true;
|
||||
// - 2: DefleMask (jump to next order unless it is the last one and ignoreJumpAtEnd is on)
|
||||
if ((curOrder<(ordersLen-1) || !ignoreJumpAtEnd)) {
|
||||
// changeOrd -2 means increase order by 1
|
||||
// it overrides a previous 0Bxx effect
|
||||
changeOrd=-2;
|
||||
changePos=effectVal;
|
||||
}
|
||||
} else if (jumpTreatment==1) {
|
||||
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd)) {
|
||||
nextOrder=i+1;
|
||||
nextRow=effectVal;
|
||||
jumpingOrder=true;
|
||||
// - 1: old Furnace (same as 2 but ignored if 0Bxx is present)
|
||||
if (changeOrd<0 && (curOrder<(ordersLen-1) || !ignoreJumpAtEnd)) {
|
||||
changeOrd=-2;
|
||||
changePos=effectVal;
|
||||
}
|
||||
} else {
|
||||
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
|
||||
if (!changingOrder) {
|
||||
nextOrder=i+1;
|
||||
// - 0: normal
|
||||
if (curOrder<(ordersLen-1) || !ignoreJumpAtEnd) {
|
||||
// set the target order if not set, allowing you to use 0B and 0D regardless of position
|
||||
if (changeOrd<0) {
|
||||
changeOrd=-2;
|
||||
}
|
||||
jumpingOrder=true;
|
||||
nextRow=effectVal;
|
||||
changePos=effectVal;
|
||||
}
|
||||
}
|
||||
} else if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0x0b) {
|
||||
if (nextOrder==-1 || jumpTreatment==0) {
|
||||
nextOrder=effectVal;
|
||||
if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) {
|
||||
nextRow=0;
|
||||
break;
|
||||
case 0xed: // delay
|
||||
if (effectVal!=0) {
|
||||
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
|
||||
// - 0: strict
|
||||
// - delays equal or greater to the speed * timeBase are ignored
|
||||
// - 1: strict old
|
||||
// - delays equal or greater to the speed are ignored
|
||||
// - 2: lax (default)
|
||||
// - no delay is ever ignored unless overridden by another
|
||||
bool comparison=(delayBehavior==1)?(effectVal<=nextSpeed):(effectVal<(nextSpeed*(timeBase+1)));
|
||||
if (delayBehavior==2) comparison=true;
|
||||
if (comparison) {
|
||||
// set the delay row, order and timer
|
||||
rowDelay[i]=effectVal;
|
||||
delayOrder[i]=whatOrder;
|
||||
delayRow[i]=whatRow;
|
||||
|
||||
// once we're done with pre-effects, get out and don't process any further
|
||||
returnAfterPre=true;
|
||||
}
|
||||
changingOrder=true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// stop processing if EDxx was found
|
||||
if (returnAfterPre) return;
|
||||
} else {
|
||||
//logV("honoring delay at position %d",whatRow);
|
||||
}
|
||||
|
||||
wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7);
|
||||
|
||||
// effects
|
||||
for (int j=0; j<pat[i].effectCols; j++) {
|
||||
short effect=p->newData[whatRow][DIV_PAT_FX(j)];
|
||||
short effectVal=p->newData[whatRow][DIV_PAT_FXVAL(j)];
|
||||
|
||||
// an empty effect value is treated as zero
|
||||
if (effectVal==-1) effectVal=0;
|
||||
effectVal&=255;
|
||||
|
||||
// tempo/tick rate effects
|
||||
switch (effect) {
|
||||
case 0xc0: case 0xc1: case 0xc2: case 0xc3: // set tick rate
|
||||
// Cxxx, where `xxx` is between 1 and 1023
|
||||
divider=(double)(((effect&0x3)<<8)|effectVal);
|
||||
if (divider<1) divider=1;
|
||||
break;
|
||||
case 0xf0: // set tempo
|
||||
divider=(double)effectVal*2.0/5.0;
|
||||
if (divider<1) divider=1;
|
||||
break;
|
||||
case 0xff: // stop song
|
||||
shallStopSched=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto tinyNextRow=[&,this]() {
|
||||
// store the previous position
|
||||
prevOrder=curOrder;
|
||||
prevRow=curRow;
|
||||
|
||||
if (songWillEnd) {
|
||||
endOfSong=true;
|
||||
}
|
||||
|
||||
for (int i=0; i<chans; i++) {
|
||||
tinyProcessRow(i,false);
|
||||
}
|
||||
|
||||
// mark this row as "walked" over
|
||||
wsWalked[((curOrder<<5)+(curRow>>3))&8191]|=1<<(curRow&7);
|
||||
|
||||
// commit a pending jump if there is one
|
||||
// otherwise, advance row position
|
||||
if (changeOrd!=-1) {
|
||||
// jump to order and reset position
|
||||
curRow=changePos;
|
||||
changePos=0;
|
||||
// jump to next order if it is -2
|
||||
if (changeOrd==-2) changeOrd=curOrder+1;
|
||||
curOrder=changeOrd;
|
||||
|
||||
// if we're out of bounds, return to the beginning
|
||||
// if this happens we're guaranteed to loop
|
||||
if (curOrder>=ordersLen) {
|
||||
curOrder=0;
|
||||
ts.isLoopDefined=false;
|
||||
songWillEnd=true;
|
||||
memset(wsWalked,0,8192);
|
||||
}
|
||||
changeOrd=-1;
|
||||
} else if (++curRow>=patLen) {
|
||||
// if we are here it means we reached the end of this pattern, so
|
||||
// advance to next order unless the song is about to stop
|
||||
if (shallStopSched) {
|
||||
curRow=patLen-1;
|
||||
} else {
|
||||
// go to next order
|
||||
curRow=0;
|
||||
if (++curOrder>=ordersLen) {
|
||||
logV("end of orders reached");
|
||||
ts.isLoopDefined=false;
|
||||
songWillEnd=true;
|
||||
// the walked array is used for loop detection
|
||||
// since we've reached the end, we are guaranteed to loop here, so
|
||||
// just reset it.
|
||||
memset(wsWalked,0,8192);
|
||||
curOrder=0;
|
||||
}
|
||||
}
|
||||
}
|
||||
rowChanged=true;
|
||||
ts.totalRows++;
|
||||
|
||||
// new loop detection routine
|
||||
// if we're stepping on a row we've already walked over, we found loop
|
||||
// if the song is going to stop though, don't do anything
|
||||
if (!songWillEnd && wsWalked[((curOrder<<5)+(curRow>>3))&8191]&(1<<(curRow&7)) && !shallStopSched) {
|
||||
logV("loop reached");
|
||||
songWillEnd=true;
|
||||
memset(wsWalked,0,8192);
|
||||
}
|
||||
// perform speed alternation
|
||||
// COMPAT FLAG: broken speed alternation
|
||||
if (brokenSpeedSel) {
|
||||
unsigned char speed2=(curSpeeds.len>=2)?curSpeeds.val[1]:curSpeeds.val[0];
|
||||
unsigned char speed1=curSpeeds.val[0];
|
||||
|
||||
if (nextOrder!=-1) {
|
||||
i=nextOrder-1;
|
||||
nextOrder=-1;
|
||||
// if the pattern length is odd and the current order is odd, use speed 2 for even rows and speed 1 for odd ones
|
||||
// we subtract firstPat from curOrder as firstPat is used by a function which finds sub-songs
|
||||
// (the beginning of a new sub-song will be in order 0)
|
||||
if ((patLen&1) && (curOrder-firstPat)&1) {
|
||||
ticks=((curRow&1)?speed2:speed1)*(timeBase+1);
|
||||
nextSpeed=(curRow&1)?speed1:speed2;
|
||||
} else {
|
||||
ticks=((curRow&1)?speed1:speed2)*(timeBase+1);
|
||||
nextSpeed=(curRow&1)?speed2:speed1;
|
||||
}
|
||||
} else {
|
||||
// normal speed alternation
|
||||
// set the number of ticks and cycle to the next speed
|
||||
ticks=curSpeeds.val[curSpeed]*(timeBase+1);
|
||||
curSpeed++;
|
||||
if (curSpeed>=curSpeeds.len) curSpeed=0;
|
||||
// cache the next speed for future operations
|
||||
nextSpeed=curSpeeds.val[curSpeed];
|
||||
}
|
||||
|
||||
if (songWillEnd && !endOfSong) {
|
||||
ts.loopEnd.order=prevOrder;
|
||||
ts.loopEnd.row=prevRow;
|
||||
}
|
||||
};
|
||||
|
||||
// MAKE IT WORK
|
||||
while (!endOfSong) {
|
||||
// cycle channels to find a tick rate/tempo change effect after delay
|
||||
// (unfortunately Cxxx and F0xx are not pre-effects and obey EDxx)
|
||||
for (int i=0; i<chans; i++) {
|
||||
if (rowDelay[i]>0) {
|
||||
if (--rowDelay[i]==0) {
|
||||
tinyProcessRow(i,true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run virtual tempo
|
||||
tempoAccum+=curVirtualTempoN;
|
||||
while (tempoAccum>=curVirtualTempoD) {
|
||||
tempoAccum-=curVirtualTempoD;
|
||||
// tick counter
|
||||
if (--ticks<=0) {
|
||||
if (shallStopSched) {
|
||||
shallStop=true;
|
||||
break;
|
||||
} else if (endOfSong) {
|
||||
break;
|
||||
}
|
||||
|
||||
// next row
|
||||
tinyNextRow();
|
||||
break;
|
||||
}
|
||||
|
||||
// limit tempo accumulator
|
||||
if (tempoAccum>1023) tempoAccum=1023;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double calcRowLenInSeconds(const DivGroovePattern& speeds, float hz, int vN, int vD, int timeBaseFromSong) {
|
||||
double hl=1; //count for 1 row
|
||||
if (hl<=0.0) hl=4.0;
|
||||
double timeBase=timeBaseFromSong+1;
|
||||
double speedSum=0;
|
||||
for (int i=0; i<MIN(16,speeds.len); i++) {
|
||||
speedSum+=speeds.val[i];
|
||||
}
|
||||
speedSum/=MAX(1,speeds.len);
|
||||
if (timeBase<1.0) timeBase=1.0;
|
||||
if (speedSum<1.0) speedSum=1.0;
|
||||
if (vD<1) vD=1;
|
||||
return 1.0/((60.0*hz/(timeBase*hl*speedSum))*(double)vN/(double)vD/60.0);
|
||||
}
|
||||
|
||||
void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders_vec, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat) {
|
||||
length=0;
|
||||
hasFFxx=false;
|
||||
rowsForFadeout=0;
|
||||
|
||||
float secondsPerThisRow=0.0f;
|
||||
|
||||
DivGroovePattern curSpeeds=speeds; //simulate that we are playing the song, track all speed/BPM/tempo/engine rate changes
|
||||
short curVirtualTempoN=virtualTempoN;
|
||||
short curVirtualTempoD=virtualTempoD;
|
||||
float curHz=hz;
|
||||
double curDivider=(double)timeBase;
|
||||
|
||||
double curLen=0.0; //how many seconds passed since the start of song
|
||||
|
||||
int nextOrder=-1;
|
||||
int nextRow=0;
|
||||
int effectVal=0;
|
||||
int lastSuspectedLoopEnd=-1;
|
||||
DivPattern* subPat[DIV_MAX_CHANS];
|
||||
unsigned char wsWalked[8192];
|
||||
memset(wsWalked,0,8192);
|
||||
if (firstPat>0) {
|
||||
memset(wsWalked,255,32*firstPat);
|
||||
}
|
||||
for (int i=firstPat; i<ordersLen; i++) {
|
||||
bool jumped=false;
|
||||
|
||||
for (int j=0; j<chans; j++) {
|
||||
subPat[j]=pat[j].getPattern(orders.ord[j][i],false);
|
||||
if (shallStop) {
|
||||
// FFxx found - the song doesn't loop
|
||||
ts.isLoopable=false;
|
||||
ts.isLoopDefined=false;
|
||||
break;
|
||||
}
|
||||
if (i>lastSuspectedLoopEnd) {
|
||||
lastSuspectedLoopEnd=i;
|
||||
}
|
||||
for (int j=nextRow; j<patLen; j++) {
|
||||
nextRow=0;
|
||||
bool changingOrder=false;
|
||||
bool jumpingOrder=false;
|
||||
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) {
|
||||
return;
|
||||
}
|
||||
for (int k=0; k<chans; k++) {
|
||||
for (int l=0; l<pat[k].effectCols; l++) {
|
||||
effectVal=subPat[k]->newData[j][DIV_PAT_FXVAL(l)];
|
||||
if (effectVal<0) effectVal=0;
|
||||
|
||||
if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0xff) {
|
||||
hasFFxx=true;
|
||||
|
||||
// FFxx makes YOU SHALL NOT PASS!!! move
|
||||
orders_vec.push_back(j+1); // order len
|
||||
length+=j+1; // add length of order to song length
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (subPat[k]->newData[j][DIV_PAT_FX(l)]) {
|
||||
case 0x09: { // select groove pattern/speed 1
|
||||
if (grooves.empty()) {
|
||||
if (effectVal>0) curSpeeds.val[0]=effectVal;
|
||||
} else {
|
||||
if (effectVal<(short)grooves.size()) {
|
||||
curSpeeds=grooves[effectVal];
|
||||
//curSpeed=0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x0f: { // speed 1/speed 2
|
||||
if (curSpeeds.len==2 && grooves.empty()) {
|
||||
if (effectVal>0) curSpeeds.val[1]=effectVal;
|
||||
} else {
|
||||
if (effectVal>0) curSpeeds.val[0]=effectVal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xfd: { // virtual tempo num
|
||||
if (effectVal>0) curVirtualTempoN=effectVal;
|
||||
break;
|
||||
}
|
||||
case 0xfe: { // virtual tempo den
|
||||
if (effectVal>0) curVirtualTempoD=effectVal;
|
||||
break;
|
||||
}
|
||||
case 0xf0: { // set Hz by tempo (set bpm)
|
||||
curDivider=(double)effectVal*2.0/5.0;
|
||||
if (curDivider<1) curDivider=1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0x0d) {
|
||||
if (jumpTreatment==2) {
|
||||
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
|
||||
nextOrder=i+1;
|
||||
nextRow=effectVal;
|
||||
jumpingOrder=true;
|
||||
}
|
||||
} else if (jumpTreatment==1) {
|
||||
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd)) {
|
||||
nextOrder=i+1;
|
||||
nextRow=effectVal;
|
||||
jumpingOrder=true;
|
||||
}
|
||||
} else {
|
||||
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
|
||||
if (!changingOrder) {
|
||||
nextOrder=i+1;
|
||||
}
|
||||
jumpingOrder=true;
|
||||
nextRow=effectVal;
|
||||
}
|
||||
}
|
||||
} else if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0x0b) {
|
||||
if (nextOrder==-1 || jumpTreatment==0) {
|
||||
nextOrder=effectVal;
|
||||
if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) {
|
||||
nextRow=0;
|
||||
}
|
||||
changingOrder=true;
|
||||
}
|
||||
}
|
||||
// log row time here
|
||||
if (rowChanged && !endOfSong) {
|
||||
if (ts.orders[prevOrder]==NULL) {
|
||||
ts.orders[prevOrder]=new TimeMicros[DIV_MAX_ROWS];
|
||||
for (int i=0; i<DIV_MAX_ROWS; i++) {
|
||||
ts.orders[prevOrder][i].seconds=-1;
|
||||
}
|
||||
}
|
||||
ts.orders[prevOrder][prevRow]=ts.totalTime;
|
||||
rowChanged=false;
|
||||
}
|
||||
|
||||
if (i>loopOrder || (i==loopOrder && j>loopRow)) {
|
||||
// we count each row fadeout lasts. When our time is greater than fadeout length we successfully counted the number of fadeout rows
|
||||
if (curLen<=fadeoutLen && fadeoutLen>0.0) {
|
||||
secondsPerThisRow=calcRowLenInSeconds(speeds,curHz,curVirtualTempoN,curVirtualTempoD,curDivider);
|
||||
curLen+=secondsPerThisRow;
|
||||
rowsForFadeout++;
|
||||
}
|
||||
if (!endOfSong) {
|
||||
// update playback time
|
||||
double dt=divider;//*((double)virtualTempoN/(double)MAX(1,virtualTempoD));
|
||||
ts.totalTicks++;
|
||||
|
||||
ts.totalTime.micros+=1000000/dt;
|
||||
totalMicrosOff+=fmod(1000000.0,dt);
|
||||
while (totalMicrosOff>=dt) {
|
||||
totalMicrosOff-=dt;
|
||||
ts.totalTime.micros++;
|
||||
}
|
||||
|
||||
wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7);
|
||||
|
||||
if (nextOrder!=-1) {
|
||||
i=nextOrder-1;
|
||||
orders_vec.push_back(j+1); // order len
|
||||
length+=j+1; // add length of order to song length
|
||||
jumped=true;
|
||||
nextOrder=-1;
|
||||
break;
|
||||
if (ts.totalTime.micros>=1000000) {
|
||||
ts.totalTime.micros-=1000000;
|
||||
// who's gonna play a song for 68 years?
|
||||
if (ts.totalTime.seconds<0x7fffffff) ts.totalTime.seconds++;
|
||||
}
|
||||
}
|
||||
if (!jumped) { // if no jump occured we add full pattern length
|
||||
orders_vec.push_back(patLen); // order len
|
||||
length+=patLen; // add length of order to song length
|
||||
}
|
||||
if (ts.maxRow[curOrder]<curRow) ts.maxRow[curOrder]=curRow;
|
||||
}
|
||||
|
||||
ts.totalRows--;
|
||||
ts.loopStart.order=prevOrder;
|
||||
ts.loopStart.row=prevRow;
|
||||
ts.loopStartTime=ts.getTimes(ts.loopStart.order,ts.loopStart.row);
|
||||
|
||||
std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now();
|
||||
logV("calcTimestamps() took %dµs",std::chrono::duration_cast<std::chrono::microseconds>(timeEnd-timeStart).count());
|
||||
}
|
||||
|
||||
void DivSubSong::clearData() {
|
||||
|
|
@ -400,24 +558,23 @@ void DivSong::findSubSongs(int chans) {
|
|||
for (DivSubSong* i: subsong) {
|
||||
std::vector<int> subSongStart;
|
||||
std::vector<int> subSongEnd;
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=-1;
|
||||
int curStart=-1;
|
||||
|
||||
// find possible subsongs
|
||||
logD("finding subsongs...");
|
||||
while (++curStart<i->ordersLen) {
|
||||
if (!i->walk(loopOrder,loopRow,loopEnd,chans,jumpTreatment,ignoreJumpAtEnd,curStart)) break;
|
||||
i->calcTimestamps(chans,grooves,jumpTreatment,ignoreJumpAtEnd,brokenSpeedSel,delayBehavior,curStart);
|
||||
if (!i->ts.isLoopable) break;
|
||||
|
||||
// make sure we don't pick the same range twice
|
||||
if (!subSongEnd.empty()) {
|
||||
if (subSongEnd.back()==loopEnd) continue;
|
||||
if (subSongEnd.back()==i->ts.loopEnd.order) continue;
|
||||
}
|
||||
|
||||
logV("found a subsong: %d-%d",curStart,loopEnd);
|
||||
logV("found a subsong: %d-%d",curStart,i->ts.loopEnd.order);
|
||||
subSongStart.push_back(curStart);
|
||||
subSongEnd.push_back(loopEnd);
|
||||
subSongEnd.push_back(i->ts.loopEnd.order);
|
||||
curStart=i->ts.loopEnd.order;
|
||||
}
|
||||
|
||||
// if this is the only song, quit
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include "../pch.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "../timeutils.h"
|
||||
#include "../ta-utils.h"
|
||||
#include "config.h"
|
||||
#include "orders.h"
|
||||
|
|
@ -167,6 +168,40 @@ struct DivGroovePattern {
|
|||
}
|
||||
};
|
||||
|
||||
struct DivSongTimestamps {
|
||||
// song duration (in seconds and microseconds)
|
||||
TimeMicros totalTime;
|
||||
int totalTicks;
|
||||
int totalRows;
|
||||
|
||||
// loop region (order/row positions)
|
||||
struct Position {
|
||||
int order, row;
|
||||
Position():
|
||||
order(0), row(0) {}
|
||||
} loopStart, loopEnd;
|
||||
// set to true if a 0Bxx effect is found and it defines a loop region
|
||||
bool isLoopDefined;
|
||||
// set to false if FFxx is found
|
||||
bool isLoopable;
|
||||
|
||||
// timestamp of a row
|
||||
// DO NOT ACCESS DIRECTLY! use the functions instead.
|
||||
// if seconds is -1, it means this row is not touched at all.
|
||||
TimeMicros* orders[DIV_MAX_PATTERNS];
|
||||
TimeMicros loopStartTime;
|
||||
|
||||
// the furthest row that the playhead goes through in an order.
|
||||
unsigned char maxRow[DIV_MAX_PATTERNS];
|
||||
|
||||
// call this function to get the timestamp of a row.
|
||||
TimeMicros getTimes(int order, int row);
|
||||
|
||||
DivSongTimestamps();
|
||||
~DivSongTimestamps();
|
||||
};
|
||||
|
||||
|
||||
struct DivSubSong {
|
||||
String name, notes;
|
||||
unsigned char hilightA, hilightB;
|
||||
|
|
@ -185,15 +220,13 @@ struct DivSubSong {
|
|||
String chanName[DIV_MAX_CHANS];
|
||||
String chanShortName[DIV_MAX_CHANS];
|
||||
|
||||
/**
|
||||
* walk through the song and determine loop position.
|
||||
*/
|
||||
bool walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);
|
||||
// song timestamps
|
||||
DivSongTimestamps ts;
|
||||
|
||||
/**
|
||||
* find song length in rows (up to specified loop point).
|
||||
* calculate timestamps (loop position, song length and more).
|
||||
*/
|
||||
void findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);
|
||||
void calcTimestamps(int chans, std::vector<DivGroovePattern>& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat=0);
|
||||
|
||||
void clearData();
|
||||
void removeUnusedPatterns();
|
||||
|
|
|
|||
|
|
@ -1272,10 +1272,9 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
|
|||
double origRate=got.rate;
|
||||
got.rate=correctedRate;
|
||||
// determine loop point
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
walkSong(loopOrder,loopRow,loopEnd);
|
||||
calcSongTimestamps();
|
||||
int loopOrder=curSubSong->ts.loopStart.order;
|
||||
int loopRow=curSubSong->ts.loopStart.row;
|
||||
logI("loop point: %d %d",loopOrder,loopRow);
|
||||
warnings="";
|
||||
|
||||
|
|
|
|||
|
|
@ -104,10 +104,9 @@ void FurnaceGUI::drawClock() {
|
|||
}
|
||||
}
|
||||
if (clockShowTime) {
|
||||
int totalTicks=e->getTotalTicks();
|
||||
int totalSeconds=e->getTotalSeconds();
|
||||
String timeFormatted=e->getCurTime().toString(2,TA_TIME_FORMAT_MS_ZERO);
|
||||
ImGui::PushFont(bigFont);
|
||||
ImGui::Text("%.2d:%.2d.%.2d",(totalSeconds/60),totalSeconds%60,totalTicks/10000);
|
||||
ImGui::TextUnformatted(timeFormatted.c_str());
|
||||
ImGui::PopFont();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ void FurnaceGUI::drawPalette() {
|
|||
showError("cannot add chip! ("+e->getLastError()+")");
|
||||
} else {
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
ImGui::CloseCurrentPopup();
|
||||
if (e->song.autoSystem) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
#include <fmt/printf.h>
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "misc/cpp/imgui_stdlib.h"
|
||||
|
||||
PendingDrawOsc _debugDo;
|
||||
static float oscDebugData[2048];
|
||||
|
|
@ -199,6 +200,41 @@ void FurnaceGUI::drawDebug() {
|
|||
ImGui::Text("patScroll: %f",patScroll);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
if (ImGui::TreeNode("Song Timestamps")) {
|
||||
if (ImGui::Button("Recalculate")) {
|
||||
e->calcSongTimestamps();
|
||||
}
|
||||
|
||||
DivSongTimestamps& ts=e->curSubSong->ts;
|
||||
|
||||
String timeFormatted=ts.totalTime.toString(-1,TA_TIME_FORMAT_AUTO);
|
||||
ImGui::Text("song duration: %s (%d ticks; %d rows)",timeFormatted.c_str(),ts.totalTicks,ts.totalRows);
|
||||
if (ts.isLoopDefined) {
|
||||
ImGui::Text("loop region is defined");
|
||||
} else {
|
||||
ImGui::Text("no loop region");
|
||||
}
|
||||
if (ts.isLoopable) {
|
||||
ImGui::Text("song can loop");
|
||||
} else {
|
||||
ImGui::Text("song will stop");
|
||||
}
|
||||
|
||||
ImGui::Text("loop region: %d:%d - %d:%d",ts.loopStart.order,ts.loopStart.row,ts.loopEnd.order,ts.loopEnd.row);
|
||||
timeFormatted=ts.loopStartTime.toString(-1,TA_TIME_FORMAT_AUTO);
|
||||
ImGui::Text("loop start time: %s",timeFormatted.c_str());
|
||||
|
||||
if (ImGui::TreeNode("Maximum rows")) {
|
||||
for (int i=0; i<e->curSubSong->ordersLen; i++) {
|
||||
ImGui::Text("- Order %d: %d",i,ts.maxRow[i]);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
ImGui::Checkbox("Enable row timestamps (in pattern view)",&debugRowTimestamps);
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
if (ImGui::TreeNode("Sample Debug")) {
|
||||
for (int i=0; i<e->song.sampleLen; i++) {
|
||||
DivSample* sample=e->getSample(i);
|
||||
|
|
@ -334,6 +370,25 @@ void FurnaceGUI::drawDebug() {
|
|||
ImGui::Unindent();
|
||||
ImGui::TreePop();
|
||||
}
|
||||
if (ImGui::TreeNode("TimeMicros Test")) {
|
||||
static TimeMicros testTS;
|
||||
static String testTSIn;
|
||||
String testTSFormatted=testTS.toString();
|
||||
ImGui::Text("Current Value: %s",testTSFormatted.c_str());
|
||||
|
||||
if (ImGui::InputText("fromString",&testTSIn)) {
|
||||
try {
|
||||
testTS=TimeMicros::fromString(testTSIn);
|
||||
} catch (std::invalid_argument& e) {
|
||||
ImGui::Text("COULD NOT! (%s)",e.what());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::InputInt("seconds",&testTS.seconds);
|
||||
ImGui::InputInt("micros",&testTS.micros);
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
if (ImGui::TreeNode("New File Picker Test")) {
|
||||
static bool check0, check1, check2, check3, check4, check5;
|
||||
|
||||
|
|
|
|||
|
|
@ -363,6 +363,9 @@ void FurnaceGUI::doAction(int what) {
|
|||
case GUI_ACTION_WINDOW_USER_PRESETS:
|
||||
nextWindow=GUI_WINDOW_USER_PRESETS;
|
||||
break;
|
||||
case GUI_ACTION_WINDOW_REF_PLAYER:
|
||||
nextWindow=GUI_WINDOW_REF_PLAYER;
|
||||
break;
|
||||
|
||||
case GUI_ACTION_COLLAPSE_WINDOW:
|
||||
collapseWindow=true;
|
||||
|
|
@ -471,6 +474,9 @@ void FurnaceGUI::doAction(int what) {
|
|||
case GUI_WINDOW_USER_PRESETS:
|
||||
userPresetsOpen=false;
|
||||
break;
|
||||
case GUI_WINDOW_REF_PLAYER:
|
||||
refPlayerOpen=false;
|
||||
break;
|
||||
case GUI_WINDOW_TUNER:
|
||||
tunerOpen=false;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -633,6 +633,10 @@ void FurnaceGUI::drawMobileControls() {
|
|||
if (ImGui::Button(_("EffectList"))) {
|
||||
effectListOpen=!effectListOpen;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(_("RefPlayer"))) {
|
||||
refPlayerOpen=!refPlayerOpen;
|
||||
}
|
||||
if (ImGui::Button(_("Switch to Desktop Mode"))) {
|
||||
toggleMobileUI(!mobileUI);
|
||||
}
|
||||
|
|
@ -755,8 +759,18 @@ void FurnaceGUI::drawEditControls() {
|
|||
|
||||
ImGui::SameLine();
|
||||
pushToggleColors(noteInputPoly);
|
||||
if (ImGui::Button(noteInputPoly?(_("Poly##PolyInput")):(_("Mono##PolyInput")))) {
|
||||
noteInputPoly=!noteInputPoly;
|
||||
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
|
||||
if (noteInputPoly) {
|
||||
if (noteInputChord) {
|
||||
noteInputPoly=false;
|
||||
noteInputChord=false;
|
||||
} else {
|
||||
noteInputChord=true;
|
||||
}
|
||||
} else {
|
||||
noteInputPoly=true;
|
||||
noteInputChord=false;
|
||||
}
|
||||
e->setAutoNotePoly(noteInputPoly);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
|
@ -885,8 +899,18 @@ void FurnaceGUI::drawEditControls() {
|
|||
|
||||
ImGui::SameLine();
|
||||
pushToggleColors(noteInputPoly);
|
||||
if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) {
|
||||
noteInputPoly=!noteInputPoly;
|
||||
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
|
||||
if (noteInputPoly) {
|
||||
if (noteInputChord) {
|
||||
noteInputPoly=false;
|
||||
noteInputChord=false;
|
||||
} else {
|
||||
noteInputChord=true;
|
||||
}
|
||||
} else {
|
||||
noteInputPoly=true;
|
||||
noteInputChord=false;
|
||||
}
|
||||
e->setAutoNotePoly(noteInputPoly);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
|
@ -1023,8 +1047,18 @@ void FurnaceGUI::drawEditControls() {
|
|||
popToggleColors();
|
||||
|
||||
pushToggleColors(noteInputPoly);
|
||||
if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) {
|
||||
noteInputPoly=!noteInputPoly;
|
||||
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
|
||||
if (noteInputPoly) {
|
||||
if (noteInputChord) {
|
||||
noteInputPoly=false;
|
||||
noteInputChord=false;
|
||||
} else {
|
||||
noteInputChord=true;
|
||||
}
|
||||
} else {
|
||||
noteInputPoly=true;
|
||||
noteInputChord=false;
|
||||
}
|
||||
e->setAutoNotePoly(noteInputPoly);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
|
@ -1123,8 +1157,18 @@ void FurnaceGUI::drawEditControls() {
|
|||
|
||||
ImGui::SameLine();
|
||||
pushToggleColors(noteInputPoly);
|
||||
if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) {
|
||||
noteInputPoly=!noteInputPoly;
|
||||
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
|
||||
if (noteInputPoly) {
|
||||
if (noteInputChord) {
|
||||
noteInputPoly=false;
|
||||
noteInputChord=false;
|
||||
} else {
|
||||
noteInputChord=true;
|
||||
}
|
||||
} else {
|
||||
noteInputPoly=true;
|
||||
noteInputChord=false;
|
||||
}
|
||||
e->setAutoNotePoly(noteInputPoly);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ void FurnaceGUI::prepareUndo(ActionType action, UndoRegion region) {
|
|||
|
||||
void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) {
|
||||
bool doPush=false;
|
||||
bool shallWalk=false;
|
||||
UndoStep s;
|
||||
s.type=action;
|
||||
s.oldCursor=undoCursor;
|
||||
|
|
@ -184,6 +183,7 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) {
|
|||
if (!s.ord.empty()) {
|
||||
doPush=true;
|
||||
}
|
||||
recalcTimestamps=true;
|
||||
break;
|
||||
case GUI_UNDO_PATTERN_EDIT:
|
||||
case GUI_UNDO_PATTERN_DELETE:
|
||||
|
|
@ -227,13 +227,29 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) {
|
|||
s.pat.push_back(UndoPatternData(subSong,i,e->curOrders->ord[i][h],j,k,op->newData[j][k],p->newData[j][k]));
|
||||
|
||||
if (k>=DIV_PAT_FX(0)) {
|
||||
if (op->newData[j][k&(~1)]==0x0b ||
|
||||
p->newData[j][k&(~1)]==0x0b ||
|
||||
op->newData[j][k&(~1)]==0x0d ||
|
||||
p->newData[j][k&(~1)]==0x0d ||
|
||||
op->newData[j][k&(~1)]==0xff ||
|
||||
p->newData[j][k&(~1)]==0xff) {
|
||||
shallWalk=true;
|
||||
int fxCol=(k&1)?k:(k-1);
|
||||
if (op->newData[j][fxCol]==0x09 ||
|
||||
op->newData[j][fxCol]==0x0b ||
|
||||
op->newData[j][fxCol]==0x0d ||
|
||||
op->newData[j][fxCol]==0x0f ||
|
||||
op->newData[j][fxCol]==0xc0 ||
|
||||
op->newData[j][fxCol]==0xc1 ||
|
||||
op->newData[j][fxCol]==0xc2 ||
|
||||
op->newData[j][fxCol]==0xc3 ||
|
||||
op->newData[j][fxCol]==0xf0 ||
|
||||
op->newData[j][fxCol]==0xff ||
|
||||
p->newData[j][fxCol]==0x09 ||
|
||||
p->newData[j][fxCol]==0x0b ||
|
||||
p->newData[j][fxCol]==0x0d ||
|
||||
p->newData[j][fxCol]==0x0f ||
|
||||
p->newData[j][fxCol]==0xc0 ||
|
||||
p->newData[j][fxCol]==0xc1 ||
|
||||
p->newData[j][fxCol]==0xc2 ||
|
||||
p->newData[j][fxCol]==0xc3 ||
|
||||
p->newData[j][fxCol]==0xf0 ||
|
||||
p->newData[j][fxCol]==0xff) {
|
||||
logV("recalcTimestamps due to speed effect.");
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,9 +274,6 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) {
|
|||
redoHist.clear();
|
||||
if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front();
|
||||
}
|
||||
if (shallWalk) {
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
}
|
||||
|
||||
// garbage collection
|
||||
for (std::pair<unsigned short,DivPattern*> i: oldPatMap) {
|
||||
|
|
@ -1798,6 +1811,7 @@ void FurnaceGUI::doCollapseSong(int divider) {
|
|||
redoHist.clear();
|
||||
if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front();
|
||||
}
|
||||
recalcTimestamps=true;
|
||||
|
||||
if (e->isPlaying()) e->play();
|
||||
}
|
||||
|
|
@ -1874,6 +1888,7 @@ void FurnaceGUI::doExpandSong(int multiplier) {
|
|||
redoHist.clear();
|
||||
if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front();
|
||||
}
|
||||
recalcTimestamps=true;
|
||||
|
||||
if (e->isPlaying()) e->play();
|
||||
}
|
||||
|
|
@ -2065,6 +2080,7 @@ void FurnaceGUI::moveSelected(int x, int y) {
|
|||
// replace
|
||||
cursor=selStart;
|
||||
doPaste(GUI_PASTE_MODE_OVERFLOW,0,false,c);
|
||||
recalcTimestamps=true;
|
||||
|
||||
makeUndo(GUI_UNDO_PATTERN_DRAG,UndoRegion(firstOrder,0,0,lastOrder,e->getTotalChannelCount()-1,e->curSubSong->patLen-1));
|
||||
}
|
||||
|
|
@ -2119,10 +2135,11 @@ void FurnaceGUI::doUndo() {
|
|||
}
|
||||
}
|
||||
}
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
break;
|
||||
}
|
||||
|
||||
recalcTimestamps=true;
|
||||
|
||||
bool shallReplay=false;
|
||||
for (UndoOtherData& i: us.other) {
|
||||
switch (i.target) {
|
||||
|
|
@ -2197,10 +2214,11 @@ void FurnaceGUI::doRedo() {
|
|||
}
|
||||
}
|
||||
}
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
break;
|
||||
}
|
||||
|
||||
recalcTimestamps=true;
|
||||
|
||||
bool shallReplay=false;
|
||||
for (UndoOtherData& i: us.other) {
|
||||
switch (i.target) {
|
||||
|
|
|
|||
|
|
@ -459,6 +459,7 @@ void FurnaceGUI::doReplace() {
|
|||
if (!curQueryResults.empty()) {
|
||||
MARK_MODIFIED;
|
||||
}
|
||||
recalcTimestamps=true;
|
||||
|
||||
if (!us.pat.empty()) {
|
||||
undoHist.push_back(us);
|
||||
|
|
|
|||
241
src/gui/gui.cpp
241
src/gui/gui.cpp
|
|
@ -1283,7 +1283,6 @@ void FurnaceGUI::play(int row) {
|
|||
chanOscChan[i].pitch=0.0f;
|
||||
}
|
||||
memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float));
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS);
|
||||
if (wasFollowing) {
|
||||
followPattern=true;
|
||||
|
|
@ -1302,6 +1301,7 @@ void FurnaceGUI::play(int row) {
|
|||
}
|
||||
curNibble=false;
|
||||
orderNibble=false;
|
||||
chordInputOffset=0;
|
||||
activeNotes.clear();
|
||||
}
|
||||
|
||||
|
|
@ -1314,10 +1314,10 @@ void FurnaceGUI::setOrder(unsigned char order, bool forced) {
|
|||
|
||||
void FurnaceGUI::stop() {
|
||||
bool wasPlaying=e->isPlaying();
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->stop();
|
||||
curNibble=false;
|
||||
orderNibble=false;
|
||||
chordInputOffset=0;
|
||||
if (followPattern && wasPlaying) {
|
||||
nextScroll=-1.0f;
|
||||
nextAddScroll=0.0f;
|
||||
|
|
@ -1360,13 +1360,24 @@ void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode, bool autoNote) {
|
|||
}
|
||||
}
|
||||
|
||||
void FurnaceGUI::noteInput(int num, int key, int vol) {
|
||||
void FurnaceGUI::noteInput(int num, int key, int vol, int chanOff) {
|
||||
int ch=cursor.xCoarse;
|
||||
int ord=curOrder;
|
||||
int y=cursor.y;
|
||||
int tick=0;
|
||||
int speed=0;
|
||||
|
||||
if (chanOff>0 && noteInputChord) {
|
||||
ch=e->getViableChannel(ch,chanOff,curIns);
|
||||
if ((!e->isPlaying() || !followPattern)) {
|
||||
y-=editStep;
|
||||
while (y<0) {
|
||||
if (--ord<0) ord=0;
|
||||
y+=e->curSubSong->patLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e->isPlaying() && !e->isStepping() && followPattern) {
|
||||
e->getPlayPosTick(ord,y,tick,speed);
|
||||
if (tick<=(speed/2)) { // round
|
||||
|
|
@ -1380,12 +1391,12 @@ void FurnaceGUI::noteInput(int num, int key, int vol) {
|
|||
}
|
||||
}
|
||||
|
||||
logV("chan %d, %d:%d %d/%d",ch,ord,y,tick,speed);
|
||||
logV("noteInput: chan %d, offset %d, %d:%d %d/%d",ch,chanOff,ord,y,tick,speed);
|
||||
|
||||
DivPattern* pat=e->curPat[ch].getPattern(e->curOrders->ord[ch][ord],true);
|
||||
bool removeIns=false;
|
||||
|
||||
prepareUndo(GUI_UNDO_PATTERN_EDIT);
|
||||
prepareUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y));
|
||||
|
||||
if (key==GUI_NOTE_OFF) { // note off
|
||||
pat->newData[y][DIV_PAT_NOTE]=DIV_NOTE_OFF;
|
||||
|
|
@ -1423,8 +1434,10 @@ void FurnaceGUI::noteInput(int num, int key, int vol) {
|
|||
pat->newData[y][DIV_PAT_VOL]=-1;
|
||||
}
|
||||
}
|
||||
editAdvance();
|
||||
makeUndo(GUI_UNDO_PATTERN_EDIT);
|
||||
if ((!e->isPlaying() || !followPattern) && (chanOff<1 || !noteInputChord)) {
|
||||
editAdvance();
|
||||
}
|
||||
makeUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y));
|
||||
curNibble=false;
|
||||
}
|
||||
|
||||
|
|
@ -1433,6 +1446,8 @@ void FurnaceGUI::valueInput(int num, bool direct, int target) {
|
|||
int ord=curOrder;
|
||||
int y=cursor.y;
|
||||
|
||||
logV("valueInput: chan %d, %d:%d",ch,ord,y);
|
||||
|
||||
if (e->isPlaying() && !e->isStepping() && followPattern) {
|
||||
e->getPlayPos(ord,y);
|
||||
}
|
||||
|
|
@ -1543,7 +1558,6 @@ void FurnaceGUI::orderInput(int num) {
|
|||
}
|
||||
}
|
||||
}
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
makeUndo(GUI_UNDO_CHANGE_ORDER);
|
||||
}
|
||||
}
|
||||
|
|
@ -1742,8 +1756,9 @@ void FurnaceGUI::keyDown(SDL_Event& ev) {
|
|||
if (num>119) num=119; // B-9
|
||||
|
||||
if (edit) {
|
||||
noteInput(num,key);
|
||||
noteInput(num,key,-1,chordInputOffset);
|
||||
}
|
||||
chordInputOffset++;
|
||||
}
|
||||
} else if (edit) { // value
|
||||
auto it=valueKeys.find(ev.key.keysym.sym);
|
||||
|
|
@ -1839,7 +1854,10 @@ void FurnaceGUI::keyDown(SDL_Event& ev) {
|
|||
}
|
||||
|
||||
void FurnaceGUI::keyUp(SDL_Event& ev) {
|
||||
// nothing for now
|
||||
// this is very, very lazy...
|
||||
if (--chordInputOffset<0) {
|
||||
chordInputOffset=0;
|
||||
}
|
||||
}
|
||||
|
||||
bool dirExists(String s) {
|
||||
|
|
@ -2316,6 +2334,17 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
|
|||
dpiScale
|
||||
);
|
||||
break;
|
||||
case GUI_FILE_MUSIC_OPEN:
|
||||
if (!dirExists(workingDirMusic)) workingDirMusic=getHomeDir();
|
||||
hasOpened=fileDialog->openLoad(
|
||||
_("Open Audio File"),
|
||||
audioLoadFormats,
|
||||
workingDirMusic,
|
||||
dpiScale,
|
||||
NULL,
|
||||
false
|
||||
);
|
||||
break;
|
||||
case GUI_FILE_TEST_OPEN:
|
||||
if (!dirExists(workingDirTest)) workingDirTest=getHomeDir();
|
||||
hasOpened=fileDialog->openLoad(
|
||||
|
|
@ -2553,7 +2582,7 @@ int FurnaceGUI::load(String path) {
|
|||
}
|
||||
pushRecentFile(path);
|
||||
// walk song
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
e->calcSongTimestamps();
|
||||
// do not auto-play a backup
|
||||
if (path.find(backupPath)!=0) {
|
||||
if (settings.playOnLoad==2 || (settings.playOnLoad==1 && wasPlaying)) {
|
||||
|
|
@ -2711,20 +2740,11 @@ int FurnaceGUI::loadStream(String path) {
|
|||
|
||||
|
||||
void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
|
||||
songOrdersLengths.clear();
|
||||
e->calcSongTimestamps();
|
||||
DivSongTimestamps& ts=e->curSubSong->ts;
|
||||
|
||||
int loopOrder=0;
|
||||
int loopRow=0;
|
||||
int loopEnd=0;
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
|
||||
e->findSongLength(loopOrder,loopRow,audioExportOptions.fadeOut,songFadeoutSectionLength,songHasSongEndCommand,songOrdersLengths,songLength); // for progress estimation
|
||||
|
||||
songLoopedSectionLength=songLength;
|
||||
for (int i=0; i<loopOrder && i<(int)songOrdersLengths.size(); i++) {
|
||||
songLoopedSectionLength-=songOrdersLengths[i];
|
||||
}
|
||||
songLoopedSectionLength-=loopRow;
|
||||
songLength=ts.totalTime.toDouble();
|
||||
double loopLength=songLength-(ts.loopStartTime.seconds+(double)ts.loopStartTime.micros/1000000.0);
|
||||
|
||||
e->saveAudio(path.c_str(),audioExportOptions);
|
||||
|
||||
|
|
@ -2732,19 +2752,15 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
|
|||
e->getTotalAudioFiles(totalFiles);
|
||||
int totalLoops=0;
|
||||
|
||||
lengthOfOneFile=songLength;
|
||||
|
||||
if (!songHasSongEndCommand) {
|
||||
if (ts.isLoopable) {
|
||||
e->getTotalLoops(totalLoops);
|
||||
|
||||
lengthOfOneFile+=songLoopedSectionLength*totalLoops;
|
||||
lengthOfOneFile+=songFadeoutSectionLength; // account for fadeout
|
||||
songLength+=loopLength*totalLoops;
|
||||
songLength+=audioExportOptions.fadeOut;
|
||||
}
|
||||
|
||||
totalLength=lengthOfOneFile*totalFiles;
|
||||
totalLength=songLength*totalFiles;
|
||||
|
||||
curProgress=0.0f;
|
||||
|
||||
displayExporting=true;
|
||||
}
|
||||
|
||||
|
|
@ -3848,6 +3864,7 @@ bool FurnaceGUI::loop() {
|
|||
DECLARE_METRIC(log)
|
||||
DECLARE_METRIC(effectList)
|
||||
DECLARE_METRIC(userPresets)
|
||||
DECLARE_METRIC(refPlayer)
|
||||
DECLARE_METRIC(popup)
|
||||
|
||||
#ifdef IS_MOBILE
|
||||
|
|
@ -3869,6 +3886,7 @@ bool FurnaceGUI::loop() {
|
|||
|
||||
while (!quit) {
|
||||
SDL_Event ev;
|
||||
SelectionPoint prevCursor=cursor;
|
||||
if (e->isPlaying()) {
|
||||
WAKE_UP;
|
||||
}
|
||||
|
|
@ -4003,6 +4021,9 @@ bool FurnaceGUI::loop() {
|
|||
break;
|
||||
case SDL_KEYUP:
|
||||
// for now
|
||||
if (!ImGui::GetIO().WantCaptureKeyboard || (newFilePicker->isOpened() && !ImGui::GetIO().WantTextInput)) {
|
||||
keyUp(ev);
|
||||
}
|
||||
insEditMayBeDirty=true;
|
||||
if (introPos<11.0 && introSkip<0.5 && !shortIntro) {
|
||||
introSkipDo=false;
|
||||
|
|
@ -4186,14 +4207,19 @@ bool FurnaceGUI::loop() {
|
|||
if (action!=0) {
|
||||
doAction(action);
|
||||
} else switch (msg.type&0xf0) {
|
||||
case TA_MIDI_NOTE_OFF:
|
||||
if (--chordInputOffset<0) chordInputOffset=0;
|
||||
break;
|
||||
case TA_MIDI_NOTE_ON:
|
||||
if (midiMap.valueInputStyle==0 || midiMap.valueInputStyle>3 || cursor.xFine==0) {
|
||||
if (midiMap.noteInput && edit && msg.data[1]!=0) {
|
||||
noteInput(
|
||||
msg.data[0]-12,
|
||||
0,
|
||||
midiMap.volInput?msg.data[1]:-1
|
||||
midiMap.volInput?msg.data[1]:-1,
|
||||
chordInputOffset
|
||||
);
|
||||
chordInputOffset++;
|
||||
}
|
||||
} else {
|
||||
if (edit && msg.data[1]!=0) {
|
||||
|
|
@ -4462,6 +4488,7 @@ bool FurnaceGUI::loop() {
|
|||
IMPORT_CLOSE(memoryOpen);
|
||||
IMPORT_CLOSE(csPlayerOpen);
|
||||
IMPORT_CLOSE(userPresetsOpen);
|
||||
IMPORT_CLOSE(refPlayerOpen);
|
||||
} else if (pendingLayoutImportStep==1) {
|
||||
// let the UI settle
|
||||
} else if (pendingLayoutImportStep==2) {
|
||||
|
|
@ -4648,6 +4675,7 @@ bool FurnaceGUI::loop() {
|
|||
showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError()));
|
||||
} else {
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
ImGui::CloseCurrentPopup();
|
||||
if (e->song.autoSystem) {
|
||||
|
|
@ -4677,6 +4705,7 @@ bool FurnaceGUI::loop() {
|
|||
if (picked!=DIV_SYSTEM_NULL) {
|
||||
if (e->changeSystem(i,picked,preserveChanPos)) {
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
if (e->song.autoSystem) {
|
||||
autoDetectSystem();
|
||||
}
|
||||
|
|
@ -4701,6 +4730,7 @@ bool FurnaceGUI::loop() {
|
|||
showError(fmt::sprintf(_("cannot remove chip! (%s)"),e->getLastError()));
|
||||
} else {
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (e->song.autoSystem) {
|
||||
autoDetectSystem();
|
||||
|
|
@ -4832,6 +4862,7 @@ bool FurnaceGUI::loop() {
|
|||
if (ImGui::MenuItem(_("effect list"),BIND_FOR(GUI_ACTION_WINDOW_EFFECT_LIST),effectListOpen)) effectListOpen=!effectListOpen;
|
||||
if (ImGui::MenuItem(_("play/edit controls"),BIND_FOR(GUI_ACTION_WINDOW_EDIT_CONTROLS),editControlsOpen)) editControlsOpen=!editControlsOpen;
|
||||
if (ImGui::MenuItem(_("piano/input pad"),BIND_FOR(GUI_ACTION_WINDOW_PIANO),pianoOpen)) pianoOpen=!pianoOpen;
|
||||
if (ImGui::MenuItem(_("reference music player"),BIND_FOR(GUI_ACTION_WINDOW_REF_PLAYER),refPlayerOpen)) refPlayerOpen=!refPlayerOpen;
|
||||
if (spoilerOpen) if (ImGui::MenuItem(_("spoiler"),NULL,spoilerOpen)) spoilerOpen=!spoilerOpen;
|
||||
|
||||
ImGui::EndMenu();
|
||||
|
|
@ -4850,8 +4881,7 @@ bool FurnaceGUI::loop() {
|
|||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PLAYBACK_STAT]);
|
||||
if (e->isPlaying() && settings.playbackTime) {
|
||||
int totalTicks=e->getTotalTicks();
|
||||
int totalSeconds=e->getTotalSeconds();
|
||||
TimeMicros totalTime=e->getCurTime();
|
||||
|
||||
String info;
|
||||
|
||||
|
|
@ -4880,32 +4910,10 @@ bool FurnaceGUI::loop() {
|
|||
|
||||
info+=_("| ");
|
||||
|
||||
if (totalSeconds==0x7fffffff) {
|
||||
if (totalTime.seconds==0x7fffffff) {
|
||||
info+=_("Don't you have anything better to do?");
|
||||
} else {
|
||||
if (totalSeconds>=86400) {
|
||||
int totalDays=totalSeconds/86400;
|
||||
int totalYears=totalDays/365;
|
||||
totalDays%=365;
|
||||
int totalMonths=totalDays/30;
|
||||
totalDays%=30;
|
||||
|
||||
#ifdef HAVE_LOCALE
|
||||
info+=fmt::sprintf(ngettext("%d year ","%d years ",totalYears),totalYears);
|
||||
info+=fmt::sprintf(ngettext("%d month ","%d months ",totalMonths),totalMonths);
|
||||
info+=fmt::sprintf(ngettext("%d day ","%d days ",totalDays),totalDays);
|
||||
#else
|
||||
info+=fmt::sprintf(_GN("%d year ","%d years ",totalYears),totalYears);
|
||||
info+=fmt::sprintf(_GN("%d month ","%d months ",totalMonths),totalMonths);
|
||||
info+=fmt::sprintf(_GN("%d day ","%d days ",totalDays),totalDays);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (totalSeconds>=3600) {
|
||||
info+=fmt::sprintf("%.2d:",(totalSeconds/3600)%24);
|
||||
}
|
||||
|
||||
info+=fmt::sprintf("%.2d:%.2d.%.2d",(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000);
|
||||
info+=totalTime.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO);
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted(info.c_str());
|
||||
|
|
@ -5054,6 +5062,7 @@ bool FurnaceGUI::loop() {
|
|||
MEASURE(memory,drawMemory());
|
||||
MEASURE(effectList,drawEffectList());
|
||||
MEASURE(userPresets,drawUserPresets());
|
||||
MEASURE(refPlayer,drawRefPlayer());
|
||||
MEASURE(patManager,drawPatManager());
|
||||
|
||||
} else {
|
||||
|
|
@ -5101,6 +5110,7 @@ bool FurnaceGUI::loop() {
|
|||
MEASURE(log,drawLog());
|
||||
MEASURE(effectList,drawEffectList());
|
||||
MEASURE(userPresets,drawUserPresets());
|
||||
MEASURE(refPlayer,drawRefPlayer());
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -5252,6 +5262,9 @@ bool FurnaceGUI::loop() {
|
|||
case GUI_FILE_CMDSTREAM_OPEN:
|
||||
workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR;
|
||||
break;
|
||||
case GUI_FILE_MUSIC_OPEN:
|
||||
workingDirMusic=fileDialog->getPath()+DIR_SEPARATOR_STR;
|
||||
break;
|
||||
case GUI_FILE_TEST_OPEN:
|
||||
case GUI_FILE_TEST_OPEN_MULTI:
|
||||
case GUI_FILE_TEST_SAVE:
|
||||
|
|
@ -5890,6 +5903,17 @@ bool FurnaceGUI::loop() {
|
|||
showError(fmt::sprintf(_("Error while loading file! (%s)"),lastError));
|
||||
}
|
||||
break;
|
||||
case GUI_FILE_MUSIC_OPEN:
|
||||
e->synchronizedSoft([this,copyOfName]() {
|
||||
bool wasPlaying=e->getFilePlayer()->isPlaying();
|
||||
if (!e->getFilePlayer()->loadFile(copyOfName.c_str())) {
|
||||
showError(fmt::sprintf(_("Error while loading file!")));
|
||||
} else if (wasPlaying && filePlayerSync && refPlayerOpen && e->isPlaying()) {
|
||||
e->syncFilePlayer();
|
||||
e->getFilePlayer()->play();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case GUI_FILE_TEST_OPEN:
|
||||
showWarning(fmt::sprintf(_("You opened: %s"),copyOfName),GUI_WARN_GENERIC);
|
||||
break;
|
||||
|
|
@ -6025,46 +6049,21 @@ bool FurnaceGUI::loop() {
|
|||
if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_CHAN) {
|
||||
ImGui::Text(_("Please wait..."));
|
||||
}
|
||||
float* progressLambda=&curProgress;
|
||||
int curPosInRows=0;
|
||||
int* curPosInRowsLambda=&curPosInRows;
|
||||
int loopsLeft=0;
|
||||
int* loopsLeftLambda=&loopsLeft;
|
||||
int totalLoops=0;
|
||||
int* totalLoopsLambda=&totalLoops;
|
||||
int curFile=0;
|
||||
int* curFileLambda=&curFile;
|
||||
if (e->isExporting()) {
|
||||
e->lockEngine(
|
||||
[this, progressLambda, curPosInRowsLambda, curFileLambda, loopsLeftLambda, totalLoopsLambda] () {
|
||||
int curRow=0; int curOrder=0;
|
||||
e->getCurSongPos(curRow, curOrder);
|
||||
[this, curFileLambda] () {
|
||||
*curFileLambda=0;
|
||||
e->getCurFileIndex(*curFileLambda);
|
||||
*curPosInRowsLambda=curRow;
|
||||
for (int i=0; i<MIN(curOrder,(int)songOrdersLengths.size()); i++) *curPosInRowsLambda+=songOrdersLengths[i];
|
||||
if (!songHasSongEndCommand) {
|
||||
e->getLoopsLeft(*loopsLeftLambda);
|
||||
e->getTotalLoops(*totalLoopsLambda);
|
||||
if ((*totalLoopsLambda)!=(*loopsLeftLambda)) { // we are going 2nd, 3rd, etc. time through the song
|
||||
*curPosInRowsLambda-=(songLength-songLoopedSectionLength); // a hack so progress bar does not jump?
|
||||
}
|
||||
if (e->getIsFadingOut()) { // we are in fadeout??? why it works like that bruh
|
||||
// LIVE WITH IT damn it
|
||||
*curPosInRowsLambda-=(songLength-songLoopedSectionLength); // a hack so progress bar does not jump?
|
||||
}
|
||||
}
|
||||
if (totalLength<0.1) {
|
||||
// DON'T
|
||||
*progressLambda=0;
|
||||
} else {
|
||||
*progressLambda=(float)((*curPosInRowsLambda)+((*totalLoopsLambda)-(*loopsLeftLambda))*songLength+lengthOfOneFile*(*curFileLambda))/(float)totalLength;
|
||||
}
|
||||
curProgress=(e->getCurTime().toDouble()+(songLength*(*curFileLambda)))/totalLength;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ImGui::Text(_("Row %d of %d"),curPosInRows+((totalLoops)-(loopsLeft))*songLength,lengthOfOneFile);
|
||||
if (curProgress<0.0f) curProgress=0.0f;
|
||||
if (curProgress>1.0f) curProgress=1.0f;
|
||||
|
||||
if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles);
|
||||
|
||||
ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0),fmt::sprintf("%.2f%%",curProgress*100.0f).c_str());
|
||||
|
|
@ -6515,6 +6514,7 @@ bool FurnaceGUI::loop() {
|
|||
selStart.order=0;
|
||||
selEnd.order=0;
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::Button(_("Current subsong"))) {
|
||||
|
|
@ -6528,6 +6528,7 @@ bool FurnaceGUI::loop() {
|
|||
selStart.order=0;
|
||||
selEnd.order=0;
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::Button(_("Orders"))) {
|
||||
|
|
@ -6542,6 +6543,7 @@ bool FurnaceGUI::loop() {
|
|||
selStart.order=0;
|
||||
selEnd.order=0;
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::Button(_("Pattern"))) {
|
||||
|
|
@ -6553,6 +6555,7 @@ bool FurnaceGUI::loop() {
|
|||
}
|
||||
});
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::Button(_("Instruments"))) {
|
||||
|
|
@ -6596,6 +6599,7 @@ bool FurnaceGUI::loop() {
|
|||
e->curSubSong->rearrangePatterns();
|
||||
});
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::Button(_("Remove unused patterns"))) {
|
||||
|
|
@ -6604,6 +6608,7 @@ bool FurnaceGUI::loop() {
|
|||
e->curSubSong->removeUnusedPatterns();
|
||||
});
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui::Button(_("Remove unused instruments"))) {
|
||||
|
|
@ -6657,6 +6662,7 @@ bool FurnaceGUI::loop() {
|
|||
selStart=cursor;
|
||||
selEnd=cursor;
|
||||
curOrder=0;
|
||||
recalcTimestamps=true;
|
||||
MARK_MODIFIED;
|
||||
}
|
||||
ImGui::CloseCurrentPopup();
|
||||
|
|
@ -6675,6 +6681,7 @@ bool FurnaceGUI::loop() {
|
|||
MARK_MODIFIED;
|
||||
}
|
||||
updateROMExportAvail();
|
||||
recalcTimestamps=true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
|
@ -7387,6 +7394,26 @@ bool FurnaceGUI::loop() {
|
|||
}
|
||||
}
|
||||
|
||||
if (recalcTimestamps) {
|
||||
logV("need to recalc timestamps...");
|
||||
e->calcSongTimestamps();
|
||||
recalcTimestamps=false;
|
||||
}
|
||||
|
||||
if (!e->isPlaying() && e->getFilePlayerSync()) {
|
||||
if (cursor.y!=prevCursor.y || cursor.order!=prevCursor.order) {
|
||||
DivFilePlayer* fp=e->getFilePlayer();
|
||||
logV("cursor moved to %d:%d",cursor.order,cursor.y);
|
||||
if (!fp->isPlaying()) {
|
||||
TimeMicros rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y);
|
||||
if (rowTS.seconds!=-1) {
|
||||
TimeMicros cueTime=e->getFilePlayerCue();
|
||||
fp->setPosSeconds(cueTime+rowTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sampleMapWaitingInput=(curWindow==GUI_WINDOW_INS_EDIT && sampleMapFocused);
|
||||
|
||||
curWindowThreadSafe=curWindow;
|
||||
|
|
@ -7436,6 +7463,9 @@ bool FurnaceGUI::loop() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reset chord count just in case
|
||||
chordInputOffset=0;
|
||||
}
|
||||
|
||||
if (!settings.renderClearPos || renderBackend==GUI_BACKEND_METAL) {
|
||||
|
|
@ -8183,11 +8213,13 @@ void FurnaceGUI::syncState() {
|
|||
workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir);
|
||||
workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir);
|
||||
workingDirROMExport=e->getConfString("lastDirROMExport",workingDir);
|
||||
workingDirROM=e->getConfString("lastDirROM",workingDir);
|
||||
workingDirFont=e->getConfString("lastDirFont",workingDir);
|
||||
workingDirColors=e->getConfString("lastDirColors",workingDir);
|
||||
workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir);
|
||||
workingDirLayout=e->getConfString("lastDirLayout",workingDir);
|
||||
workingDirConfig=e->getConfString("lastDirConfig",workingDir);
|
||||
workingDirMusic=e->getConfString("lastDirMusic",workingDir);
|
||||
workingDirTest=e->getConfString("lastDirTest",workingDir);
|
||||
|
||||
editControlsOpen=e->getConfBool("editControlsOpen",true);
|
||||
|
|
@ -8231,6 +8263,7 @@ void FurnaceGUI::syncState() {
|
|||
findOpen=e->getConfBool("findOpen",false);
|
||||
spoilerOpen=e->getConfBool("spoilerOpen",false);
|
||||
userPresetsOpen=e->getConfBool("userPresetsOpen",false);
|
||||
refPlayerOpen=e->getConfBool("refPlayerOpen",false);
|
||||
|
||||
insListDir=e->getConfBool("insListDir",false);
|
||||
waveListDir=e->getConfBool("waveListDir",false);
|
||||
|
|
@ -8266,6 +8299,8 @@ void FurnaceGUI::syncState() {
|
|||
followOrders=e->getConfBool("followOrders",true);
|
||||
followPattern=e->getConfBool("followPattern",true);
|
||||
noteInputPoly=e->getConfBool("noteInputPoly",true);
|
||||
noteInputChord=e->getConfBool("noteInputChord",false);
|
||||
filePlayerSync=e->getConfBool("filePlayerSync",true);
|
||||
audioExportOptions.loops=e->getConfInt("exportLoops",0);
|
||||
if (audioExportOptions.loops<0) audioExportOptions.loops=0;
|
||||
audioExportOptions.fadeOut=e->getConfDouble("exportFadeOut",0.0);
|
||||
|
|
@ -8354,11 +8389,13 @@ void FurnaceGUI::commitState(DivConfig& conf) {
|
|||
conf.set("lastDirAudioExport",workingDirAudioExport);
|
||||
conf.set("lastDirVGMExport",workingDirVGMExport);
|
||||
conf.set("lastDirROMExport",workingDirROMExport);
|
||||
conf.set("lastDirROM",workingDirROM);
|
||||
conf.set("lastDirFont",workingDirFont);
|
||||
conf.set("lastDirColors",workingDirColors);
|
||||
conf.set("lastDirKeybinds",workingDirKeybinds);
|
||||
conf.set("lastDirLayout",workingDirLayout);
|
||||
conf.set("lastDirConfig",workingDirConfig);
|
||||
conf.set("lastDirMusic",workingDirMusic);
|
||||
conf.set("lastDirTest",workingDirTest);
|
||||
|
||||
// commit last open windows
|
||||
|
|
@ -8399,6 +8436,7 @@ void FurnaceGUI::commitState(DivConfig& conf) {
|
|||
conf.set("findOpen",findOpen);
|
||||
conf.set("spoilerOpen",spoilerOpen);
|
||||
conf.set("userPresetsOpen",userPresetsOpen);
|
||||
conf.set("refPlayerOpen",refPlayerOpen);
|
||||
|
||||
// commit dir state
|
||||
conf.set("insListDir",insListDir);
|
||||
|
|
@ -8430,6 +8468,8 @@ void FurnaceGUI::commitState(DivConfig& conf) {
|
|||
conf.set("followPattern",followPattern);
|
||||
conf.set("orderEditMode",orderEditMode);
|
||||
conf.set("noteInputPoly",noteInputPoly);
|
||||
conf.set("noteInputChord",noteInputChord);
|
||||
conf.set("filePlayerSync",filePlayerSync);
|
||||
if (settings.persistFadeOut) {
|
||||
conf.set("exportLoops",audioExportOptions.loops);
|
||||
conf.set("exportFadeOut",audioExportOptions.fadeOut);
|
||||
|
|
@ -8630,8 +8670,10 @@ FurnaceGUI::FurnaceGUI():
|
|||
sysDupCloneChannels(true),
|
||||
sysDupEnd(false),
|
||||
noteInputPoly(true),
|
||||
noteInputChord(false),
|
||||
notifyWaveChange(false),
|
||||
notifySampleChange(false),
|
||||
recalcTimestamps(true),
|
||||
wantScrollListIns(false),
|
||||
wantScrollListWave(false),
|
||||
wantScrollListSample(false),
|
||||
|
|
@ -8653,15 +8695,18 @@ FurnaceGUI::FurnaceGUI():
|
|||
safeMode(false),
|
||||
midiWakeUp(true),
|
||||
makeDrumkitMode(false),
|
||||
filePlayerSync(true),
|
||||
audioEngineChanged(false),
|
||||
settingsChanged(false),
|
||||
debugFFT(false),
|
||||
debugRowTimestamps(false),
|
||||
vgmExportVersion(0x171),
|
||||
vgmExportTrailingTicks(-1),
|
||||
vgmExportCorrectedRate(44100),
|
||||
drawHalt(10),
|
||||
macroPointSize(16),
|
||||
waveEditStyle(0),
|
||||
chordInputOffset(0),
|
||||
displayInsTypeListMakeInsSample(-1),
|
||||
makeDrumkitOctave(3),
|
||||
mobileEditPage(0),
|
||||
|
|
@ -8726,12 +8771,8 @@ FurnaceGUI::FurnaceGUI():
|
|||
patFont(NULL),
|
||||
bigFont(NULL),
|
||||
headFont(NULL),
|
||||
songLength(0),
|
||||
songLoopedSectionLength(0),
|
||||
songFadeoutSectionLength(0),
|
||||
songHasSongEndCommand(false),
|
||||
lengthOfOneFile(0),
|
||||
totalLength(0),
|
||||
songLength(0.0),
|
||||
totalLength(0.0),
|
||||
curProgress(0.0f),
|
||||
totalFiles(0),
|
||||
localeRequiresJapanese(false),
|
||||
|
|
@ -8757,9 +8798,6 @@ FurnaceGUI::FurnaceGUI():
|
|||
soloChan(-1),
|
||||
orderEditMode(0),
|
||||
orderCursor(-1),
|
||||
loopOrder(-1),
|
||||
loopRow(-1),
|
||||
loopEnd(-1),
|
||||
isClipping(0),
|
||||
newSongCategory(0),
|
||||
latchTarget(0),
|
||||
|
|
@ -8830,6 +8868,7 @@ FurnaceGUI::FurnaceGUI():
|
|||
csPlayerOpen(false),
|
||||
cvOpen(false),
|
||||
userPresetsOpen(false),
|
||||
refPlayerOpen(false),
|
||||
cvNotSerious(false),
|
||||
shortIntro(false),
|
||||
insListDir(false),
|
||||
|
|
@ -9031,6 +9070,7 @@ FurnaceGUI::FurnaceGUI():
|
|||
resampleTarget(32000),
|
||||
resampleStrat(5),
|
||||
amplifyVol(100.0),
|
||||
amplifyOff(0.0),
|
||||
sampleSelStart(-1),
|
||||
sampleSelEnd(-1),
|
||||
sampleInfo(true),
|
||||
|
|
@ -9115,6 +9155,9 @@ FurnaceGUI::FurnaceGUI():
|
|||
xyOscIntensity(2.0f),
|
||||
xyOscThickness(2.0f),
|
||||
tunerPlan(NULL),
|
||||
fpCueInput(""),
|
||||
fpCueInputFailed(false),
|
||||
fpCueInputFailReason(""),
|
||||
followLog(true),
|
||||
#ifdef IS_MOBILE
|
||||
pianoOctaves(7),
|
||||
|
|
@ -9294,8 +9337,6 @@ FurnaceGUI::FurnaceGUI():
|
|||
|
||||
memset(romExportAvail,0,sizeof(bool)*DIV_ROM_MAX);
|
||||
|
||||
songOrdersLengths.clear();
|
||||
|
||||
strncpy(noteOffLabel,"OFF",32);
|
||||
strncpy(noteRelLabel,"===",32);
|
||||
strncpy(macroRelLabel,"REL",32);
|
||||
|
|
|
|||
|
|
@ -573,6 +573,7 @@ enum FurnaceGUIWindows {
|
|||
GUI_WINDOW_MEMORY,
|
||||
GUI_WINDOW_CS_PLAYER,
|
||||
GUI_WINDOW_USER_PRESETS,
|
||||
GUI_WINDOW_REF_PLAYER,
|
||||
GUI_WINDOW_SPOILER
|
||||
};
|
||||
|
||||
|
|
@ -653,6 +654,7 @@ enum FurnaceGUIFileDialogs {
|
|||
GUI_FILE_TG100_ROM_OPEN,
|
||||
GUI_FILE_MU5_ROM_OPEN,
|
||||
GUI_FILE_CMDSTREAM_OPEN,
|
||||
GUI_FILE_MUSIC_OPEN,
|
||||
|
||||
GUI_FILE_TEST_OPEN,
|
||||
GUI_FILE_TEST_OPEN_MULTI,
|
||||
|
|
@ -779,6 +781,7 @@ enum FurnaceGUIActions {
|
|||
GUI_ACTION_WINDOW_MEMORY,
|
||||
GUI_ACTION_WINDOW_CS_PLAYER,
|
||||
GUI_ACTION_WINDOW_USER_PRESETS,
|
||||
GUI_ACTION_WINDOW_REF_PLAYER,
|
||||
|
||||
GUI_ACTION_COLLAPSE_WINDOW,
|
||||
GUI_ACTION_CLOSE_WINDOW,
|
||||
|
|
@ -1696,7 +1699,7 @@ class FurnaceGUI {
|
|||
String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport;
|
||||
String workingDirVGMExport, workingDirROMExport;
|
||||
String workingDirFont, workingDirColors, workingDirKeybinds;
|
||||
String workingDirLayout, workingDirROM, workingDirTest;
|
||||
String workingDirLayout, workingDirROM, workingDirMusic, workingDirTest;
|
||||
String workingDirConfig;
|
||||
String mmlString[32];
|
||||
String mmlStringW, grooveString, grooveListString, mmlStringModTable;
|
||||
|
|
@ -1718,7 +1721,9 @@ class FurnaceGUI {
|
|||
bool vgmExportDirectStream, displayInsTypeList, displayWaveSizeList;
|
||||
bool portrait, injectBackUp, mobileMenuOpen, warnColorPushed;
|
||||
bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu;
|
||||
bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, notifyWaveChange, notifySampleChange;
|
||||
bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, noteInputChord;
|
||||
bool notifyWaveChange, notifySampleChange;
|
||||
bool recalcTimestamps;
|
||||
bool wantScrollListIns, wantScrollListWave, wantScrollListSample;
|
||||
bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString;
|
||||
bool displayPendingSamples, replacePendingSample;
|
||||
|
|
@ -1731,7 +1736,8 @@ class FurnaceGUI {
|
|||
bool safeMode;
|
||||
bool midiWakeUp;
|
||||
bool makeDrumkitMode;
|
||||
bool audioEngineChanged, settingsChanged, debugFFT;
|
||||
bool filePlayerSync;
|
||||
bool audioEngineChanged, settingsChanged, debugFFT, debugRowTimestamps;
|
||||
bool willExport[DIV_MAX_CHIPS];
|
||||
int vgmExportVersion;
|
||||
int vgmExportTrailingTicks;
|
||||
|
|
@ -1740,6 +1746,7 @@ class FurnaceGUI {
|
|||
int drawHalt;
|
||||
int macroPointSize;
|
||||
int waveEditStyle;
|
||||
int chordInputOffset;
|
||||
int displayInsTypeListMakeInsSample;
|
||||
int makeDrumkitOctave;
|
||||
int mobileEditPage;
|
||||
|
|
@ -1826,13 +1833,8 @@ class FurnaceGUI {
|
|||
char emptyLabel[32];
|
||||
char emptyLabel2[32];
|
||||
|
||||
std::vector<int> songOrdersLengths; // lengths of all orders (for drawing song export progress)
|
||||
int songLength; // length of all the song in rows
|
||||
int songLoopedSectionLength; // length of looped part of the song
|
||||
int songFadeoutSectionLength; // length of fading part of the song
|
||||
bool songHasSongEndCommand; // song has "Song end" command (FFxx)
|
||||
int lengthOfOneFile; // length of one rendering pass. song length times num of loops + fadeout
|
||||
int totalLength; // total length of render (lengthOfOneFile times num of files for per-channel export)
|
||||
double songLength; // length of the song in seconds
|
||||
double totalLength; // total length of render (songLength times num of files for per-channel export)
|
||||
float curProgress;
|
||||
int totalFiles;
|
||||
|
||||
|
|
@ -2385,7 +2387,7 @@ class FurnaceGUI {
|
|||
FixedQueue<bool*,64> pendingLayoutImportReopen;
|
||||
|
||||
int curIns, curWave, curSample, curOctave, curOrder, playOrder, prevIns, oldRow, editStep, editStepCoarse, soloChan, orderEditMode, orderCursor;
|
||||
int loopOrder, loopRow, loopEnd, isClipping, newSongCategory, latchTarget, undoOrder;
|
||||
int isClipping, newSongCategory, latchTarget, undoOrder;
|
||||
int wheelX, wheelY, dragSourceX, dragSourceXFine, dragSourceY, dragSourceOrder, dragDestinationX, dragDestinationXFine, dragDestinationY, dragDestinationOrder, oldBeat, oldBar;
|
||||
int curGroove, exitDisabledTimer;
|
||||
int curPaletteChoice, curPaletteType;
|
||||
|
|
@ -2402,7 +2404,7 @@ class FurnaceGUI {
|
|||
bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen;
|
||||
bool pianoOpen, notesOpen, tunerOpen, spectrumOpen, channelsOpen, regViewOpen, logOpen, effectListOpen, chanOscOpen;
|
||||
bool subSongsOpen, findOpen, spoilerOpen, patManagerOpen, sysManagerOpen, clockOpen, speedOpen;
|
||||
bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen;
|
||||
bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen, refPlayerOpen;
|
||||
|
||||
bool cvNotSerious;
|
||||
|
||||
|
|
@ -2631,7 +2633,7 @@ class FurnaceGUI {
|
|||
int resizeSize, silenceSize;
|
||||
double resampleTarget;
|
||||
int resampleStrat;
|
||||
float amplifyVol;
|
||||
float amplifyVol, amplifyOff;
|
||||
int sampleSelStart, sampleSelEnd;
|
||||
bool sampleInfo, sampleCompatRate;
|
||||
bool sampleDragActive, sampleDragMode, sampleDrag16, sampleZoomAuto;
|
||||
|
|
@ -2776,6 +2778,11 @@ class FurnaceGUI {
|
|||
float keyHit1[DIV_MAX_CHANS];
|
||||
int lastIns[DIV_MAX_CHANS];
|
||||
|
||||
// file player temp variables
|
||||
String fpCueInput;
|
||||
bool fpCueInputFailed;
|
||||
String fpCueInputFailReason;
|
||||
|
||||
// log window
|
||||
bool followLog;
|
||||
|
||||
|
|
@ -3036,6 +3043,7 @@ class FurnaceGUI {
|
|||
void drawTutorial();
|
||||
void drawXYOsc();
|
||||
void drawUserPresets();
|
||||
void drawRefPlayer();
|
||||
|
||||
float drawSystemChannelInfo(const DivSysDef* whichDef, int keyHitOffset=-1, float width=-1.0f);
|
||||
void drawSystemChannelInfoText(const DivSysDef* whichDef);
|
||||
|
|
@ -3118,7 +3126,7 @@ class FurnaceGUI {
|
|||
void doDrag(bool copy=false);
|
||||
void editOptions(bool topMenu);
|
||||
DivSystem systemPicker(bool fullWidth);
|
||||
void noteInput(int num, int key, int vol=-1);
|
||||
void noteInput(int num, int key, int vol=-1, int chanOff=0);
|
||||
void valueInput(int num, bool direct=false, int target=-1);
|
||||
void orderInput(int num);
|
||||
|
||||
|
|
|
|||
|
|
@ -674,6 +674,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={
|
|||
D("WINDOW_MEMORY", _N("Memory Composition"), 0),
|
||||
D("WINDOW_CS_PLAYER", _N("Command Stream Player"), 0),
|
||||
D("WINDOW_USER_PRESETS", _N("User Presets"), 0),
|
||||
D("WINDOW_REF_PLAYER", _N("Reference Music Player"), 0),
|
||||
|
||||
D("COLLAPSE_WINDOW", _N("Collapse/expand current window"), 0),
|
||||
D("CLOSE_WINDOW", _N("Close current window"), FURKMOD_SHIFT|SDLK_ESCAPE),
|
||||
|
|
|
|||
|
|
@ -362,8 +362,21 @@ void FurnaceGUI::drawMixer() {
|
|||
}
|
||||
}
|
||||
|
||||
// metronome/sample preview
|
||||
// file player/metronome/sample preview
|
||||
if (displayInternalPorts) {
|
||||
if (portSet(_("Music Player"),0xffc,0,16,0,16,selectedSubPort,portPos)) {
|
||||
selectedPortSet=0xffc;
|
||||
if (selectedSubPort>=0) {
|
||||
portDragActive=true;
|
||||
ImGui::InhibitInertialScroll();
|
||||
auto subPortI=portPos.find((selectedPortSet<<4)|selectedSubPort);
|
||||
if (subPortI!=portPos.cend()) {
|
||||
subPortPos=subPortI->second;
|
||||
} else {
|
||||
portDragActive=false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (portSet(_("Sample Preview"),0xffd,0,1,0,1,selectedSubPort,portPos)) {
|
||||
selectedPortSet=0xffd;
|
||||
if (selectedSubPort>=0) {
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ void FurnaceGUI::drawNewSong() {
|
|||
samplePos=0;
|
||||
updateSampleTex=true;
|
||||
notifySampleChange=true;
|
||||
e->calcSongTimestamps();
|
||||
selStart=SelectionPoint();
|
||||
selEnd=SelectionPoint();
|
||||
cursor=SelectionPoint();
|
||||
|
|
|
|||
|
|
@ -90,30 +90,11 @@ void FurnaceGUI::drawMobileOrderSel() {
|
|||
|
||||
// time
|
||||
if (e->isPlaying() && settings.playbackTime) {
|
||||
int totalTicks=e->getTotalTicks();
|
||||
int totalSeconds=e->getTotalSeconds();
|
||||
String info="";
|
||||
TimeMicros totalTime=e->getCurTime();
|
||||
String info=totalTime.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO);
|
||||
|
||||
if (totalSeconds==0x7fffffff) {
|
||||
if (totalTime.seconds==0x7fffffff) {
|
||||
info="∞";
|
||||
} else {
|
||||
if (totalSeconds>=86400) {
|
||||
int totalDays=totalSeconds/86400;
|
||||
int totalYears=totalDays/365;
|
||||
totalDays%=365;
|
||||
int totalMonths=totalDays/30;
|
||||
totalDays%=30;
|
||||
|
||||
info+=fmt::sprintf("%dy",totalYears);
|
||||
info+=fmt::sprintf("%dm",totalMonths);
|
||||
info+=fmt::sprintf("%dd",totalDays);
|
||||
}
|
||||
|
||||
if (totalSeconds>=3600) {
|
||||
info+=fmt::sprintf("%.2d:",(totalSeconds/3600)%24);
|
||||
}
|
||||
|
||||
info+=fmt::sprintf("%.2d:%.2d.%.2d",(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000);
|
||||
}
|
||||
|
||||
ImVec2 textSize=ImGui::CalcTextSize(info.c_str());
|
||||
|
|
@ -344,7 +325,7 @@ void FurnaceGUI::drawOrders() {
|
|||
ImGui::PopClipRect();
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]);
|
||||
bool highlightLoop=(i>=loopOrder && i<=loopEnd);
|
||||
bool highlightLoop=(i>=e->curSubSong->ts.loopStart.order && i<=e->curSubSong->ts.loopEnd.order && e->curSubSong->ts.isLoopDefined);
|
||||
if (highlightLoop) ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,ImGui::GetColorU32(uiColors[GUI_COLOR_SONG_LOOP]));
|
||||
if (settings.orderRowsBase==1) {
|
||||
snprintf(selID,4096,"%.2X##O_S%.2x",i,i);
|
||||
|
|
@ -392,7 +373,6 @@ void FurnaceGUI::drawOrders() {
|
|||
if (e->curOrders->ord[j][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[j][i]++;
|
||||
}
|
||||
});
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
makeUndo(GUI_UNDO_CHANGE_ORDER);
|
||||
} else {
|
||||
orderCursor=j;
|
||||
|
|
@ -400,7 +380,6 @@ void FurnaceGUI::drawOrders() {
|
|||
}
|
||||
} else {
|
||||
setOrder(i);
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
if (orderEditMode!=0) {
|
||||
orderCursor=j;
|
||||
curNibble=false;
|
||||
|
|
@ -441,7 +420,6 @@ void FurnaceGUI::drawOrders() {
|
|||
if (e->curOrders->ord[j][i]>0) e->curOrders->ord[j][i]--;
|
||||
}
|
||||
});
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
makeUndo(GUI_UNDO_CHANGE_ORDER);
|
||||
} else {
|
||||
orderCursor=j;
|
||||
|
|
@ -449,7 +427,6 @@ void FurnaceGUI::drawOrders() {
|
|||
}
|
||||
} else {
|
||||
setOrder(i);
|
||||
e->walkSong(loopOrder,loopRow,loopEnd);
|
||||
if (orderEditMode!=0) {
|
||||
orderCursor=j;
|
||||
curNibble=false;
|
||||
|
|
|
|||
|
|
@ -406,6 +406,16 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int
|
|||
for (int k=mustSetXOf; k<=chans; k++) {
|
||||
patChanX[k]=ImGui::GetCursorScreenPos().x;
|
||||
}
|
||||
|
||||
if (debugRowTimestamps) {
|
||||
TimeMicros rowTS=e->curSubSong->ts.getTimes(ord,i);
|
||||
if (rowTS.seconds==-1) {
|
||||
ImGui::Text("---");
|
||||
} else {
|
||||
String timeFormatted=rowTS.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO);
|
||||
ImGui::TextUnformatted(timeFormatted.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FurnaceGUI::drawPattern() {
|
||||
|
|
|
|||
243
src/gui/refPlayer.cpp
Normal file
243
src/gui/refPlayer.cpp
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
* Furnace Tracker - multi-system chiptune tracker
|
||||
* Copyright (C) 2021-2025 tildearrow and contributors
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "gui.h"
|
||||
#include <fmt/printf.h>
|
||||
#include "imgui.h"
|
||||
#include "IconsFontAwesome4.h"
|
||||
#include "misc/cpp/imgui_stdlib.h"
|
||||
|
||||
void FurnaceGUI::drawRefPlayer() {
|
||||
DivFilePlayer* fp=e->getFilePlayer();
|
||||
if (nextWindow==GUI_WINDOW_REF_PLAYER) {
|
||||
refPlayerOpen=true;
|
||||
ImGui::SetNextWindowFocus();
|
||||
nextWindow=GUI_WINDOW_NOTHING;
|
||||
}
|
||||
fp->setActive(refPlayerOpen);
|
||||
if (!refPlayerOpen) return;
|
||||
|
||||
if (ImGui::Begin("Music Player",&refPlayerOpen,globalWinFlags,_("Music Player"))) {
|
||||
bool playPosNegative=false;
|
||||
ssize_t playPos=fp->getPos();
|
||||
if (playPos<0) {
|
||||
playPos=-playPos;
|
||||
playPosNegative=true;
|
||||
}
|
||||
size_t minPos=0;
|
||||
size_t maxPos=fp->getFileInfo().frames;
|
||||
int fileRate=fp->getFileInfo().samplerate;
|
||||
if (fileRate<1) fileRate=1;
|
||||
int posHours=(playPos/fileRate)/3600;
|
||||
int posMinutes=((playPos/fileRate)/60)%60;
|
||||
int posSeconds=(playPos/fileRate)%60;
|
||||
int posMillis=(1000*(playPos%fileRate))/fileRate;
|
||||
if (fp->isLoaded()) {
|
||||
if (playPosNegative) {
|
||||
ImGui::Text("-%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis);
|
||||
} else {
|
||||
ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextUnformatted(_("no file loaded"));
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (ImGui::SliderScalar("##Position",ImGuiDataType_U64,&playPos,&minPos,&maxPos,"")) {
|
||||
fp->setPos(playPos);
|
||||
}
|
||||
|
||||
if (ImGui::Button(ICON_FA_FOLDER_OPEN "##Open")) {
|
||||
openFileDialog(GUI_FILE_MUSIC_OPEN);
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
e->synchronizedSoft([this,fp]() {
|
||||
if (!fp->closeFile()) {
|
||||
showError(_("you haven't loaded a file!"));
|
||||
}
|
||||
});
|
||||
}
|
||||
ImGui::SetItemTooltip(_("open file\n(right click to unload current file)"));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(ICON_FA_STEP_BACKWARD)) {
|
||||
// handled outside
|
||||
}
|
||||
if (fp->isPlaying()) {
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
fp->stop();
|
||||
fp->setPosSeconds(e->getFilePlayerCue());
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) {
|
||||
fp->stop();
|
||||
fp->setPos(0);
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
fp->setPosSeconds(e->getFilePlayerCue());
|
||||
}
|
||||
ImGui::SetItemTooltip(
|
||||
_("left click: go to cue position\n"
|
||||
"middle click: go to beginning\n"
|
||||
"right click: go to cue position (but don't stop)")
|
||||
);
|
||||
} else {
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
// try setting cue pos
|
||||
TimeMicros curPos=fp->getPosSeconds();
|
||||
TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0);
|
||||
if (rowTS.seconds==-1) {
|
||||
showError(_("the first row of this order isn't going to play."));
|
||||
} else {
|
||||
e->setFilePlayerCue(curPos-rowTS);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) {
|
||||
fp->setPos(0);
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem("Edit Cue Position",ImGuiPopupFlags_MouseButtonRight)) {
|
||||
ImGui::TextUnformatted(_("Set cue position at first order:"));
|
||||
TimeMicros cueTime=e->getFilePlayerCue();
|
||||
bool altered=false;
|
||||
fpCueInput=cueTime.toString(-1,TA_TIME_FORMAT_AUTO);
|
||||
pushWarningColor(false,fpCueInputFailed);
|
||||
if (ImGui::InputText("##CuePos",&fpCueInput)) {
|
||||
try {
|
||||
cueTime=TimeMicros::fromString(fpCueInput);
|
||||
altered=true;
|
||||
fpCueInputFailed=false;
|
||||
} catch (std::invalid_argument& e) {
|
||||
fpCueInputFailed=true;
|
||||
fpCueInputFailReason=e.what();
|
||||
}
|
||||
}
|
||||
if (!ImGui::IsItemActive()) {
|
||||
fpCueInputFailed=false;
|
||||
}
|
||||
if (ImGui::IsItemHovered() && fpCueInputFailed) {
|
||||
ImGui::SetTooltip("%s",fpCueInputFailReason.c_str());
|
||||
}
|
||||
popWarningColor();
|
||||
if (altered) {
|
||||
e->setFilePlayerCue(cueTime);
|
||||
}
|
||||
if (ImGui::Button(_("OK"))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::SetItemTooltip(
|
||||
_("left click: set cue position here\n"
|
||||
" - current playback time becomes position at first row of current order\n"
|
||||
"middle click: go to beginning\n"
|
||||
"right click: fine edit cue position")
|
||||
);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (fp->isPlaying()) {
|
||||
pushToggleColors(true);
|
||||
if (ImGui::Button(ICON_FA_PAUSE "##Pause")) {
|
||||
fp->stop();
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
// try setting cue pos
|
||||
TimeMicros curPos=fp->getPosSeconds();
|
||||
TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0);
|
||||
if (rowTS.seconds==-1) {
|
||||
showError(_("the first row of this order isn't going to play."));
|
||||
} else {
|
||||
e->setFilePlayerCue(curPos-rowTS);
|
||||
fp->stop();
|
||||
}
|
||||
}
|
||||
ImGui::SetItemTooltip(_("pause\n(right click to set cue position and pause)"));
|
||||
popToggleColors();
|
||||
} else {
|
||||
if (ImGui::Button(ICON_FA_PLAY "##Play")) {
|
||||
fp->play();
|
||||
}
|
||||
ImGui::SetItemTooltip(_("play"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(ICON_FA_STEP_FORWARD "##PlayPos")) {
|
||||
// handled outside
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
TimeMicros rowTS;
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y);
|
||||
} else {
|
||||
rowTS=e->curSubSong->ts.getTimes(curOrder,0);
|
||||
}
|
||||
TimeMicros cueTime=e->getFilePlayerCue();
|
||||
if (rowTS.seconds==-1) {
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
showError(_("the row that the pattern cursor is at isn't going to play. try moving the cursor."));
|
||||
} else {
|
||||
showError(_("the first row of this order isn't going to play. try another order."));
|
||||
}
|
||||
} else {
|
||||
fp->setPosSeconds(rowTS+cueTime);
|
||||
fp->play();
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered() && (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right))) {
|
||||
fp->stop();
|
||||
}
|
||||
ImGui::SetItemTooltip(_(
|
||||
"hold left click to play from current order\n"
|
||||
"hold right click to play from pattern cursor position\n"
|
||||
"release mouse button to stop"
|
||||
));
|
||||
ImGui::SameLine();
|
||||
|
||||
pushToggleColors(filePlayerSync);
|
||||
if (ImGui::Button(_("Sync"))) {
|
||||
filePlayerSync=!filePlayerSync;
|
||||
}
|
||||
ImGui::SetItemTooltip(_("synchronize playback with tracker playback"));
|
||||
popToggleColors();
|
||||
e->setFilePlayerSync(filePlayerSync);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(_("Mix:"));
|
||||
|
||||
float vol=fp->getVolume();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (ImGui::SliderFloat("##Volume",&vol,-1.0f,1.0f,_("<-- Tracker / Reference -->"))) {
|
||||
if (vol<-1.0f) vol=-1.0f;
|
||||
if (vol>1.0f) vol=1.0f;
|
||||
fp->setVolume(vol);
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
fp->setVolume(0.0f);
|
||||
}
|
||||
ImGui::SetItemTooltip(_("right click to reset"));
|
||||
|
||||
//ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10);
|
||||
}
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER;
|
||||
ImGui::End();
|
||||
|
||||
|
||||
if (!refPlayerOpen) {
|
||||
fp->stop();
|
||||
e->setFilePlayerSync(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1212,7 +1212,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
sameLineMaybe();
|
||||
ImGui::Button(ICON_FA_VOLUME_UP "##SAmplify");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("Amplify"));
|
||||
ImGui::SetTooltip(_("Amplify/Offset"));
|
||||
}
|
||||
if (openSampleAmplifyOpt) {
|
||||
openSampleAmplifyOpt=false;
|
||||
|
|
@ -1226,6 +1226,11 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("(%.1fdB)",20.0*log10(amplifyVol/100.0f));
|
||||
ImGui::Text(_("DC offset"));
|
||||
if (ImGui::InputFloat("##Offset",&lifyOff,-100.0,100.0,"%g%%")) {
|
||||
if (amplifyOff<-100) amplifyOff=-100;
|
||||
if (amplifyOff>100) amplifyOff=100;
|
||||
}
|
||||
if (ImGui::Button(_("Apply"))) {
|
||||
sample->prepareUndo(true);
|
||||
e->lockEngine([this,sample]() {
|
||||
|
|
@ -1233,15 +1238,17 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
float vol=amplifyVol/100.0f;
|
||||
|
||||
if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) {
|
||||
float off=32767.0f*(amplifyOff/100.0f);
|
||||
for (unsigned int i=start; i<end; i++) {
|
||||
float val=sample->data16[i]*vol;
|
||||
float val=off+sample->data16[i]*vol;
|
||||
if (val<-32768) val=-32768;
|
||||
if (val>32767) val=32767;
|
||||
sample->data16[i]=val;
|
||||
}
|
||||
} else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) {
|
||||
float off=127.0f*(amplifyOff/100.0f);
|
||||
for (unsigned int i=start; i<end; i++) {
|
||||
float val=sample->data8[i]*vol;
|
||||
float val=off+sample->data8[i]*vol;
|
||||
if (val<-128) val=-128;
|
||||
if (val>127) val=127;
|
||||
sample->data8[i]=val;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (setHz<1) setHz=1;
|
||||
if (setHz>999) setHz=999;
|
||||
e->setSongRate(setHz);
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (tempoView) {
|
||||
ImGui::SameLine();
|
||||
|
|
@ -82,6 +83,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
e->curSubSong->speeds.len=1;
|
||||
});
|
||||
if (e->isPlaying()) play();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("click for one speed"));
|
||||
|
|
@ -94,6 +96,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
e->curSubSong->speeds.val[3]=e->curSubSong->speeds.val[1];
|
||||
});
|
||||
if (e->isPlaying()) play();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("click for groove pattern"));
|
||||
|
|
@ -105,6 +108,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
e->curSubSong->speeds.val[1]=e->curSubSong->speeds.val[0];
|
||||
});
|
||||
if (e->isPlaying()) play();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("click for two (alternating) speeds"));
|
||||
|
|
@ -138,6 +142,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
e->curSubSong->speeds.val[i]=intVersion[i];
|
||||
}
|
||||
});
|
||||
recalcTimestamps=true;
|
||||
if (e->isPlaying()) play();
|
||||
MARK_MODIFIED;
|
||||
}
|
||||
|
|
@ -151,6 +156,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (ImGui::InputScalar("##Speed1",ImGuiDataType_U8,&e->curSubSong->speeds.val[0],&_ONE,&_THREE)) { MARK_MODIFIED
|
||||
if (e->curSubSong->speeds.val[0]<1) e->curSubSong->speeds.val[0]=1;
|
||||
if (e->isPlaying()) play();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (e->curSubSong->speeds.len>1) {
|
||||
ImGui::SameLine();
|
||||
|
|
@ -158,6 +164,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (ImGui::InputScalar("##Speed2",ImGuiDataType_U8,&e->curSubSong->speeds.val[1],&_ONE,&_THREE)) { MARK_MODIFIED
|
||||
if (e->curSubSong->speeds.val[1]<1) e->curSubSong->speeds.val[1]=1;
|
||||
if (e->isPlaying()) play();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,6 +179,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (e->curSubSong->virtualTempoN<1) e->curSubSong->virtualTempoN=1;
|
||||
if (e->curSubSong->virtualTempoN>255) e->curSubSong->virtualTempoN=255;
|
||||
e->virtualTempoChanged();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("Numerator"));
|
||||
|
|
@ -182,6 +190,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (e->curSubSong->virtualTempoD<1) e->curSubSong->virtualTempoD=1;
|
||||
if (e->curSubSong->virtualTempoD>255) e->curSubSong->virtualTempoD=255;
|
||||
e->virtualTempoChanged();
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("Denominator (set to base tempo)"));
|
||||
|
|
@ -198,6 +207,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (realTB<1) realTB=1;
|
||||
if (realTB>16) realTB=16;
|
||||
e->curSubSong->timeBase=realTB-1;
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%.2f BPM",calcBPM(e->curSubSong->speeds,e->curSubSong->hz,e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD));
|
||||
|
|
@ -237,6 +247,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (patLen<1) patLen=1;
|
||||
if (patLen>DIV_MAX_PATTERNS) patLen=DIV_MAX_PATTERNS;
|
||||
e->curSubSong->patLen=patLen;
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
|
@ -253,6 +264,7 @@ void FurnaceGUI::drawSpeed(bool asChild) {
|
|||
if (curOrder>=ordLen) {
|
||||
setOrder(ordLen-1);
|
||||
}
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) {
|
|||
if (ImGui::Selectable(id,i==e->getCurrentSubSong())) {
|
||||
makeCursorUndo();
|
||||
e->changeSongP(i);
|
||||
recalcTimestamps=true;
|
||||
updateScroll(0);
|
||||
oldRow=0;
|
||||
cursor.xCoarse=0;
|
||||
|
|
@ -76,6 +77,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) {
|
|||
} else {
|
||||
makeCursorUndo();
|
||||
e->changeSongP(e->song.subsong.size()-1);
|
||||
recalcTimestamps=true;
|
||||
updateScroll(0);
|
||||
oldRow=0;
|
||||
cursor.xCoarse=0;
|
||||
|
|
@ -98,6 +100,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) {
|
|||
} else {
|
||||
makeCursorUndo();
|
||||
e->changeSongP(e->song.subsong.size()-1);
|
||||
recalcTimestamps=true;
|
||||
updateScroll(0);
|
||||
oldRow=0;
|
||||
cursor.xCoarse=0;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ void FurnaceGUI::drawSysManager() {
|
|||
if (picked!=DIV_SYSTEM_NULL) {
|
||||
if (e->changeSystem(i,picked,preserveChanPos)) {
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
if (e->song.autoSystem) {
|
||||
autoDetectSystem();
|
||||
}
|
||||
|
|
@ -179,6 +180,7 @@ void FurnaceGUI::drawSysManager() {
|
|||
showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError()));
|
||||
} else {
|
||||
MARK_MODIFIED;
|
||||
recalcTimestamps=true;
|
||||
}
|
||||
if (e->song.autoSystem) {
|
||||
autoDetectSystem();
|
||||
|
|
|
|||
|
|
@ -1082,7 +1082,7 @@ void FurnaceGUI::drawTutorial() {
|
|||
oneQuarter=(oneQuarter*e->curSubSong->virtualTempoN)/e->curSubSong->virtualTempoD;
|
||||
oneQuarter/=e->curSubSong->hz;
|
||||
oneQuarter/=4;
|
||||
if (cv->playSongs && e->getTotalSeconds()>=oneQuarter) {
|
||||
if (cv->playSongs && e->getCurTime().seconds>=oneQuarter) {
|
||||
if (loadRandomDemoSong()) {
|
||||
cv->loadInstruments();
|
||||
e->changeSongP(0);
|
||||
|
|
|
|||
|
|
@ -253,8 +253,8 @@ bool finishLogFile() {
|
|||
|
||||
// flush
|
||||
logFileLockI.lock();
|
||||
logFileNotify.notify_one();
|
||||
while (!iAmReallyDead) {
|
||||
logFileNotify.notify_one();
|
||||
std::this_thread::yield();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
|
|
|||
404
src/timeutils.cpp
Normal file
404
src/timeutils.cpp
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
/**
|
||||
* Furnace Tracker - multi-system chiptune tracker
|
||||
* Copyright (C) 2021-2025 tildearrow and contributors
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "timeutils.h"
|
||||
#include <fmt/printf.h>
|
||||
#include <stdexcept>
|
||||
|
||||
String TimeMicros::toString(signed char prec, TATimeFormats hms) {
|
||||
String ret;
|
||||
int actualSeconds=seconds;
|
||||
int actualMicros=micros;
|
||||
bool negative=(seconds<0);
|
||||
if (seconds<0 && micros!=0) {
|
||||
actualSeconds++;
|
||||
actualMicros=1000000-micros;
|
||||
}
|
||||
if (negative) actualSeconds=-actualSeconds;
|
||||
|
||||
// handle auto formats
|
||||
switch (hms) {
|
||||
case TA_TIME_FORMAT_AUTO:
|
||||
hms=TA_TIME_FORMAT_SECONDS;
|
||||
if (actualSeconds>60) hms=TA_TIME_FORMAT_MS;
|
||||
if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS;
|
||||
if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS;
|
||||
break;
|
||||
case TA_TIME_FORMAT_AUTO_ZERO:
|
||||
hms=TA_TIME_FORMAT_SECONDS;
|
||||
if (actualSeconds>60) hms=TA_TIME_FORMAT_MS_ZERO;
|
||||
if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS_ZERO;
|
||||
if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS_ZERO;
|
||||
break;
|
||||
case TA_TIME_FORMAT_AUTO_MS:
|
||||
hms=TA_TIME_FORMAT_MS;
|
||||
if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS;
|
||||
if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS;
|
||||
break;
|
||||
case TA_TIME_FORMAT_AUTO_MS_ZERO:
|
||||
hms=TA_TIME_FORMAT_MS_ZERO;
|
||||
if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS_ZERO;
|
||||
if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS_ZERO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (hms) {
|
||||
case TA_TIME_FORMAT_SECONDS: { // seconds
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%d",actualSeconds);
|
||||
} else {
|
||||
ret=fmt::sprintf("%d",actualSeconds);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TA_TIME_FORMAT_MS: { // m:ss
|
||||
int m=actualSeconds/60;
|
||||
int s=actualSeconds%60;
|
||||
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%d:%02d",m,s);
|
||||
} else {
|
||||
ret=fmt::sprintf("%d:%02d",m,s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TA_TIME_FORMAT_HMS: { // h:mm:ss
|
||||
int h=actualSeconds/3600;
|
||||
int m=(actualSeconds/60)%60;
|
||||
int s=actualSeconds%60;
|
||||
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%d:%02d:%02d",h,m,s);
|
||||
} else {
|
||||
ret=fmt::sprintf("%d:%02d:%02d",h,m,s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TA_TIME_FORMAT_MS_ZERO: { // mm:ss
|
||||
int m=actualSeconds/60;
|
||||
int s=actualSeconds%60;
|
||||
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%02d:%02d",m,s);
|
||||
} else {
|
||||
ret=fmt::sprintf("%02d:%02d",m,s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TA_TIME_FORMAT_HMS_ZERO: { // hh:mm:ss
|
||||
int h=actualSeconds/3600;
|
||||
int m=(actualSeconds/60)%60;
|
||||
int s=actualSeconds%60;
|
||||
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%02d:%02d:%02d",h,m,s);
|
||||
} else {
|
||||
ret=fmt::sprintf("%02d:%02d:%02d",h,m,s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TA_TIME_FORMAT_DAYS_HMS: { // ?y?m?d h:mm:ss
|
||||
int days=actualSeconds/86400;
|
||||
int years=days/365;
|
||||
days%=365;
|
||||
int months=days/30;
|
||||
days%=30;
|
||||
int h=(actualSeconds/3600)%24;
|
||||
int m=(actualSeconds/60)%60;
|
||||
int s=actualSeconds%60;
|
||||
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%dy%dm%dd %d:%02d:%02d",years,months,days,h,m,s);
|
||||
} else {
|
||||
ret=fmt::sprintf("%dy%dm%dd %d:%02d:%02d",years,months,days,h,m,s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TA_TIME_FORMAT_DAYS_HMS_ZERO: { // ?y?m?d hh:mm:ss
|
||||
int days=actualSeconds/86400;
|
||||
int years=days/365;
|
||||
days%=365;
|
||||
int months=days/30;
|
||||
days%=30;
|
||||
int h=(actualSeconds/3600)%24;
|
||||
int m=(actualSeconds/60)%60;
|
||||
int s=actualSeconds%60;
|
||||
|
||||
if (negative) {
|
||||
ret=fmt::sprintf("-%dy%dm%dd %02d:%02d:%02d",years,months,days,h,m,s);
|
||||
} else {
|
||||
ret=fmt::sprintf("%dy%dm%dd %02d:%02d:%02d",years,months,days,h,m,s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return "ERROR!";
|
||||
}
|
||||
|
||||
if (prec<0) {
|
||||
// automatic precision
|
||||
prec=6;
|
||||
int precMicros=actualMicros;
|
||||
while ((precMicros%10)==0 && prec>1) {
|
||||
prec--;
|
||||
precMicros/=10;
|
||||
}
|
||||
}
|
||||
|
||||
if (prec>0) {
|
||||
if (prec>6) prec=6;
|
||||
switch (prec) {
|
||||
case 1:
|
||||
ret+=fmt::sprintf(".%01d",actualMicros/100000);
|
||||
break;
|
||||
case 2:
|
||||
ret+=fmt::sprintf(".%02d",actualMicros/10000);
|
||||
break;
|
||||
case 3:
|
||||
ret+=fmt::sprintf(".%03d",actualMicros/1000);
|
||||
break;
|
||||
case 4:
|
||||
ret+=fmt::sprintf(".%04d",actualMicros/100);
|
||||
break;
|
||||
case 5:
|
||||
ret+=fmt::sprintf(".%05d",actualMicros/10);
|
||||
break;
|
||||
case 6:
|
||||
ret+=fmt::sprintf(".%06d",actualMicros);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
TimeMicros TimeMicros::fromString(const String& s) {
|
||||
bool seenYears=false;
|
||||
bool seenMonths=false;
|
||||
bool seenDays=false;
|
||||
bool seenDecimal=false;
|
||||
bool needSomething=true;
|
||||
bool canNegative=true;
|
||||
bool isNegative=false;
|
||||
unsigned int element[4];
|
||||
unsigned int elementDigits[4];
|
||||
int elementCount=0;
|
||||
int curElement=0;
|
||||
int curElementDigits=0;
|
||||
|
||||
int years=0;
|
||||
int months=0;
|
||||
int days=0;
|
||||
|
||||
element[0]=0;
|
||||
element[1]=0;
|
||||
element[2]=0;
|
||||
element[3]=0;
|
||||
elementDigits[0]=0;
|
||||
elementDigits[1]=0;
|
||||
elementDigits[2]=0;
|
||||
elementDigits[3]=0;
|
||||
|
||||
for (char i: s) {
|
||||
switch (i) {
|
||||
// numbers
|
||||
case '0': case '1': case '2': case '3': case '4':
|
||||
case '5': case '6': case '7': case '8': case '9':
|
||||
if (curElementDigits<9) {
|
||||
curElement*=10;
|
||||
curElement+=(i-'0');
|
||||
curElementDigits++;
|
||||
needSomething=false;
|
||||
} else {
|
||||
throw std::invalid_argument("value out of range");
|
||||
}
|
||||
break;
|
||||
|
||||
// negative indicator
|
||||
case '-':
|
||||
if (!canNegative) {
|
||||
throw std::invalid_argument("negative symbol shall be at the beginning");
|
||||
}
|
||||
isNegative=true;
|
||||
break;
|
||||
|
||||
// element separator
|
||||
case ':':
|
||||
if (needSomething) {
|
||||
throw std::invalid_argument("missing value");
|
||||
}
|
||||
if (seenDecimal) {
|
||||
throw std::invalid_argument("after decimal");
|
||||
}
|
||||
if (elementCount>=3) {
|
||||
throw std::invalid_argument("too many elements");
|
||||
}
|
||||
element[elementCount]=curElement;
|
||||
elementDigits[elementCount++]=curElementDigits;
|
||||
curElement=0;
|
||||
curElementDigits=0;
|
||||
break;
|
||||
|
||||
// decimal separator
|
||||
case '.': case ',':
|
||||
if (needSomething && elementCount!=0) {
|
||||
throw std::invalid_argument("wrong decimal separator");
|
||||
}
|
||||
if (elementCount>=3) {
|
||||
throw std::invalid_argument("too many elements");
|
||||
}
|
||||
element[elementCount]=curElement;
|
||||
elementDigits[elementCount++]=curElementDigits;
|
||||
curElement=0;
|
||||
curElementDigits=0;
|
||||
needSomething=true;
|
||||
seenDecimal=true;
|
||||
break;
|
||||
|
||||
// year/month/day marker
|
||||
case 'Y': case 'y':
|
||||
if (seenYears) {
|
||||
throw std::invalid_argument("already have years");
|
||||
}
|
||||
if (elementCount!=0) {
|
||||
throw std::invalid_argument("years must be before time");
|
||||
}
|
||||
years=curElement;
|
||||
curElement=0;
|
||||
curElementDigits=0;
|
||||
needSomething=true;
|
||||
seenYears=true;
|
||||
break;
|
||||
case 'M': case 'm':
|
||||
if (seenMonths) {
|
||||
throw std::invalid_argument("already have months");
|
||||
}
|
||||
if (elementCount!=0) {
|
||||
throw std::invalid_argument("months must be before time");
|
||||
}
|
||||
months=curElement;
|
||||
curElement=0;
|
||||
curElementDigits=0;
|
||||
needSomething=true;
|
||||
seenMonths=true;
|
||||
break;
|
||||
case 'D': case 'd':
|
||||
if (seenDays) {
|
||||
throw std::invalid_argument("already have days");
|
||||
}
|
||||
if (elementCount!=0) {
|
||||
throw std::invalid_argument("days must be before time");
|
||||
}
|
||||
days=curElement;
|
||||
curElement=0;
|
||||
curElementDigits=0;
|
||||
needSomething=true;
|
||||
seenDays=true;
|
||||
break;
|
||||
|
||||
// ignore spaces
|
||||
case ' ':
|
||||
break;
|
||||
|
||||
// fail if any other character is seen
|
||||
default:
|
||||
throw std::invalid_argument("invalid character");
|
||||
break;
|
||||
}
|
||||
canNegative=false;
|
||||
}
|
||||
|
||||
// fail if you didn't provide an element
|
||||
if (needSomething) {
|
||||
throw std::invalid_argument("missing value at the end");
|
||||
}
|
||||
|
||||
element[elementCount]=curElement;
|
||||
elementDigits[elementCount]=curElementDigits;
|
||||
|
||||
// safety checks (yeah I know these are a bit off but whatever)
|
||||
if (years>68 || months>828 || (years*365+months*30+days)>24855) {
|
||||
throw std::invalid_argument("years/months/days out of range");
|
||||
}
|
||||
|
||||
// now combine elements
|
||||
TimeMicros ret;
|
||||
ret.seconds=86400*(years*365+months*30+days);
|
||||
if (seenDecimal || elementCount==3) {
|
||||
if (elementDigits[elementCount]<6) {
|
||||
for (int i=elementDigits[elementCount]; i<6; i++) {
|
||||
element[elementCount]*=10;
|
||||
}
|
||||
} else if (elementDigits[elementCount]>6) {
|
||||
for (int i=elementDigits[elementCount]; i>6; i--) {
|
||||
element[elementCount]/=10;
|
||||
}
|
||||
}
|
||||
ret.micros=element[elementCount];
|
||||
|
||||
elementCount--;
|
||||
}
|
||||
switch (elementCount) {
|
||||
case 0: // seconds only
|
||||
ret.seconds+=element[0];
|
||||
break;
|
||||
case 1: // minutes and seconds
|
||||
if (element[0]>=35791394) {
|
||||
throw std::invalid_argument("minutes out of range");
|
||||
}
|
||||
if (element[1]>=60) {
|
||||
throw std::invalid_argument("seconds out of range");
|
||||
}
|
||||
ret.seconds+=(element[0]*60)+element[1];
|
||||
break;
|
||||
case 2: // hours, minutes and seconds
|
||||
if (seenYears || seenMonths || seenDays) {
|
||||
if (element[0]>=24) {
|
||||
throw std::invalid_argument("hours out of range");
|
||||
}
|
||||
} else {
|
||||
if (element[0]>=596523) {
|
||||
throw std::invalid_argument("hours out of range");
|
||||
}
|
||||
}
|
||||
if (element[1]>=60) {
|
||||
throw std::invalid_argument("minutes out of range");
|
||||
}
|
||||
if (element[2]>=60) {
|
||||
throw std::invalid_argument("seconds out of range");
|
||||
}
|
||||
ret.seconds+=(element[0]*3600)+(element[1]*60)+element[2];
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("shouldn't happen");
|
||||
break;
|
||||
}
|
||||
|
||||
if (isNegative) {
|
||||
ret.seconds=-ret.seconds;
|
||||
if (ret.micros!=0) {
|
||||
ret.micros=1000000-ret.micros;
|
||||
ret.seconds--;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
127
src/timeutils.h
Normal file
127
src/timeutils.h
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* Furnace Tracker - multi-system chiptune tracker
|
||||
* Copyright (C) 2021-2025 tildearrow and contributors
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef _TIMEUTILS_H
|
||||
#define _TIMEUTILS_H
|
||||
|
||||
#ifdef __unix__
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#endif
|
||||
|
||||
#include "ta-utils.h"
|
||||
|
||||
enum TATimeFormats: unsigned char {
|
||||
TA_TIME_FORMAT_SECONDS=0, // seconds only
|
||||
TA_TIME_FORMAT_MS, // m:ss
|
||||
TA_TIME_FORMAT_HMS, // h:mm:ss
|
||||
|
||||
TA_TIME_FORMAT_MS_ZERO, // mm:ss
|
||||
TA_TIME_FORMAT_HMS_ZERO, // hh:mm:ss
|
||||
|
||||
TA_TIME_FORMAT_DAYS_HMS, // years, months, days, h:mm:ss
|
||||
TA_TIME_FORMAT_DAYS_HMS_ZERO, // years, months, days, hh:mm:ss
|
||||
|
||||
TA_TIME_FORMAT_AUTO, // automatic
|
||||
TA_TIME_FORMAT_AUTO_ZERO, // automatic (zero)
|
||||
TA_TIME_FORMAT_AUTO_MS, // automatic (from minutes)
|
||||
TA_TIME_FORMAT_AUTO_MS_ZERO, // automatic (from minutes; zero)
|
||||
};
|
||||
|
||||
struct TimeMicros {
|
||||
int seconds, micros;
|
||||
|
||||
inline float toFloat() {
|
||||
return seconds+(float)micros/1000000.0f;
|
||||
}
|
||||
inline double toDouble() {
|
||||
return seconds+(double)micros/1000000.0;
|
||||
}
|
||||
|
||||
// operators
|
||||
inline TimeMicros& operator+(TimeMicros& other) {
|
||||
seconds+=other.seconds;
|
||||
micros+=other.micros;
|
||||
while (micros>=1000000) {
|
||||
micros-=1000000;
|
||||
seconds++;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
inline TimeMicros& operator+(int other) {
|
||||
seconds+=other;
|
||||
return *this;
|
||||
}
|
||||
inline TimeMicros& operator-(TimeMicros& other) {
|
||||
seconds-=other.seconds;
|
||||
micros-=other.micros;
|
||||
while (micros<0) {
|
||||
micros+=1000000;
|
||||
seconds--;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
inline TimeMicros& operator-(int other) {
|
||||
seconds-=other;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// comparison operators
|
||||
inline bool operator==(TimeMicros& other) {
|
||||
return (seconds==other.seconds && micros==other.micros);
|
||||
}
|
||||
inline bool operator>(TimeMicros& other) {
|
||||
return (seconds>other.seconds || (seconds==other.seconds && micros>other.micros));
|
||||
}
|
||||
inline bool operator>=(TimeMicros& other) {
|
||||
return (seconds>other.seconds || (seconds==other.seconds && micros>=other.micros));
|
||||
}
|
||||
|
||||
inline bool operator!=(TimeMicros& other) {
|
||||
return !(*this==other);
|
||||
}
|
||||
inline bool operator<(TimeMicros& other) {
|
||||
return !(*this>=other);
|
||||
}
|
||||
inline bool operator<=(TimeMicros& other) {
|
||||
return !(*this>other);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert this TimeMicros to a String.
|
||||
* @param prec maximum digit precision (0-6). use -1 for automatic precision.
|
||||
* @param hms how many denominations to include (0: seconds, 1: minutes, 2: hours).
|
||||
* @return formatted TimeMicros.
|
||||
*/
|
||||
String toString(signed char prec=6, TATimeFormats hms=TA_TIME_FORMAT_SECONDS);
|
||||
static TimeMicros fromString(const String& s);
|
||||
|
||||
TimeMicros(int s, int u):
|
||||
seconds(s), micros(u) {}
|
||||
#ifdef __unix__
|
||||
TimeMicros(struct timeval tv):
|
||||
seconds(tv.tv_sec), micros(tv.tv_usec) {}
|
||||
TimeMicros(struct timespec ts):
|
||||
seconds(ts.tv_sec), micros(ts.tv_nsec/1000) {}
|
||||
#endif
|
||||
TimeMicros():
|
||||
seconds(0), micros(0) {}
|
||||
};
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue