Integrate CCN 2025-2026 files
This commit is contained in:
477
public/system-files/Documentation.orc
Normal file
477
public/system-files/Documentation.orc
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ _ _____ _____ _ _ _____ _
|
||||||
|
| | (_) | (_) / __ \/ __ \| \ | | / __ \ | |
|
||||||
|
| | ___ _____ ___ ___ __| |_ _ __ __ _ | / \/| / \/| \| | | / \/ ___ __| | ___
|
||||||
|
| | | \ \ / / _ \ / __/ _ \ / _` | | '_ \ / _` | | | | | | . ` | | | / _ \ / _` |/ _ \
|
||||||
|
| |___| |\ V / __/ | (_| (_) | (_| | | | | | (_| | | \__/\| \__/\| |\ | | \__/\ (_) | (_| | __/
|
||||||
|
\_____/_| \_/ \___| \___\___/ \__,_|_|_| |_|\__, | \____/ \____/\_| \_/ \____/\___/ \__,_|\___|
|
||||||
|
__/ |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
|
||||||
|
; Ce document contient toutes les informations sur les fonctions (mots) utilisables
|
||||||
|
; dans la plateforme de livecoding.
|
||||||
|
; Vous pouvez copier-coller les exemples dans le fichier "livecode.orc".
|
||||||
|
|
||||||
|
|
||||||
|
_
|
||||||
|
| |
|
||||||
|
| |_ ___ _ __ ___ _ __ ___
|
||||||
|
| __/ _ \ '_ ` _ \| '_ \ / _ \
|
||||||
|
| || __/ | | | | | |_) | (_) |
|
||||||
|
\__\___|_| |_| |_| .__/ \___/
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
; Pour modifier le tempo, il faut utiliser la fonction de tempo
|
||||||
|
; Par défaut, le tempo est de 1, ce qui signifie 1 pulsation par seconde
|
||||||
|
; Si on met 2, la pulsation est donc de ... 2 pulsations par secondes
|
||||||
|
tempo(2) ; [0.001 - 10]
|
||||||
|
; La modification du tempo modifiera la vitesse du rythme de TOUS les sons en train d'être joués
|
||||||
|
|
||||||
|
; Un attracteur chaotique gère d'autres paramètres du temps, on peut changer sa vitesse en écrivant
|
||||||
|
; Il influence notamment la vitesse d'évolution des modes "sauvages" (voir les Sons)
|
||||||
|
chaos_speed(4) ; [0.1 - 100]
|
||||||
|
|
||||||
|
; Pour entendre le tempo, on peut utiliser le metronome (sans accent ici pour éviter l'erreur)
|
||||||
|
metronome
|
||||||
|
|
||||||
|
|
||||||
|
_____
|
||||||
|
/ ___|
|
||||||
|
\ `--. ___ _ __ ___
|
||||||
|
`--. \/ _ \| '_ \/ __|
|
||||||
|
/\__/ / (_) | | | \__ \
|
||||||
|
\____/ \___/|_| |_|___/
|
||||||
|
|
||||||
|
; "ping" - son artificiel percussif brillant
|
||||||
|
; Paramètres :
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
ping(beat(4), 0.3, 50, 1/4)
|
||||||
|
|
||||||
|
; "buzzy" - Un son plus doux que Ping et dont le rythme interne peut être imprévisible
|
||||||
|
; Paramètres :
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
buzzy(beat(1/4), 0.5, 50, 4)
|
||||||
|
|
||||||
|
; "glidy" - un son qui glisse vers la note choisie
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
glidy(beat(1), 0.5, 60, 1)
|
||||||
|
|
||||||
|
; "darkwave" - une vague sombre, qui gronde quand elle devient forte
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
darkwave(beat(1/4), 0.6, 30, 4)
|
||||||
|
|
||||||
|
; "brightwave" - une vague brillante et lumineuse
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
brightwave(beat(1/4), 0.6, 30, 4)
|
||||||
|
|
||||||
|
; "buzzwave" - une vague qui fait bzzz
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
buzzwave(beat(1/4), 0.6, 30, 4)
|
||||||
|
|
||||||
|
; "noisywave" - une vague avec un bruit de fond, comme l'écume de la mer
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
noisywave(beat(1/4), 0.6, 30, 4)
|
||||||
|
|
||||||
|
; "crunchy" - un son percussif qui croustille
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note de base de l'accord) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
crunchy(beat(4), 0.6, 20, 1/4)
|
||||||
|
|
||||||
|
; Strike : un son percussif et très saturé (distordu, sauvage)
|
||||||
|
; Paramètres :
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
strike(beat(1), 0.9, 8, 1)
|
||||||
|
|
||||||
|
; Smooth : un son doux, assez rond
|
||||||
|
; Paramètres :
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
smooth(beat(1/3), 0.5, 20, 4)
|
||||||
|
|
||||||
|
; Bounce : un son qui rebondit et qui brille !
|
||||||
|
; Paramètres :
|
||||||
|
; - Rythme
|
||||||
|
; - Volume [0-1]
|
||||||
|
; - Hauteur (note) [0-100]
|
||||||
|
; - Durée [>0]
|
||||||
|
bounce(beat(1/2), 0.5, 30, 3)
|
||||||
|
|
||||||
|
______ _ _ _ _ _
|
||||||
|
| ___| | | (_) | | | | | |
|
||||||
|
| |_ ___ _ __ ___| |_ _ ___ _ __ ___ __| | ___ _ __ _ _| |_| |__ _ __ ___ ___
|
||||||
|
| _/ _ \| '_ \ / __| __| |/ _ \| '_ \/ __| / _` |/ _ \ | '__| | | | __| '_ \| '_ ` _ \ / _ \
|
||||||
|
| || (_) | | | | (__| |_| | (_) | | | \__ \ | (_| | __/ | | | |_| | |_| | | | | | | | | __/
|
||||||
|
\_| \___/|_| |_|\___|\__|_|\___/|_| |_|___/ \__,_|\___| |_| \__, |\__|_| |_|_| |_| |_|\___|
|
||||||
|
__/ |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
|
||||||
|
; "beat" - pulsation régulière
|
||||||
|
;Paramètres :
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
beat(2) // 2 pulsations par temps
|
||||||
|
|
||||||
|
; "swing" - pulsation régulière + une pulsation swing
|
||||||
|
; Paramètres :
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
; - Swing [0 - 1]
|
||||||
|
swing(2, 0.75)
|
||||||
|
swing(2, 3/4)
|
||||||
|
|
||||||
|
; "rhythm" - rythme écrit sous la forme d'une liste
|
||||||
|
; Paramètres :
|
||||||
|
; - liste de 1 et 0 sous forme d'un "array"
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
rhythm(array(1, 0, 0, 1, 0, 0, 1, 1 ), 1)
|
||||||
|
; Peut aussi s'écrire
|
||||||
|
rhythm(
|
||||||
|
array(1, 0, 0, 1),
|
||||||
|
1)
|
||||||
|
|
||||||
|
; "drunk" - rythme aléatoire mais quand même lié au tempo
|
||||||
|
; Paramètres :
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
; - Taux d'aléatoire [0 - 1]
|
||||||
|
drunk(2, 0.8)
|
||||||
|
|
||||||
|
; "euclidian" - rythmes basés sur la division euclidienne
|
||||||
|
; (voir https://dbkaplun.github.io/euclidean-rhythm/)
|
||||||
|
; Paramètres :
|
||||||
|
; - Vitesse [> 0]
|
||||||
|
; - Nombre de "steps" [> 0]
|
||||||
|
; - Division du cercle [> 0]
|
||||||
|
; - Rotation [0 - 1]
|
||||||
|
euclidian(2, 3, 7, 0)
|
||||||
|
|
||||||
|
; "chaos_rhythm" - rythmes basés sur l'horloge chaotique principale
|
||||||
|
; Paramètres
|
||||||
|
; - Choix de l'horloge [1, 2 ou 3]
|
||||||
|
; - Seuil de détection [0 - 1]
|
||||||
|
chaos_rhythm(1, 0.6)
|
||||||
|
|
||||||
|
|
||||||
|
______ _ _ _ _ _
|
||||||
|
| ___| | | (_) | | | | | |
|
||||||
|
| |_ ___ _ __ ___| |_ _ ___ _ __ ___ __| | ___ ___ ___ _ __ | |_ _ __ ___ | | ___
|
||||||
|
| _/ _ \| '_ \ / __| __| |/ _ \| '_ \/ __| / _` |/ _ \ / __/ _ \| '_ \| __| '__/ _ \| |/ _ \
|
||||||
|
| || (_) | | | | (__| |_| | (_) | | | \__ \ | (_| | __/ | (_| (_) | | | | |_| | | (_) | | __/
|
||||||
|
\_| \___/|_| |_|\___|\__|_|\___/|_| |_|___/ \__,_|\___| \___\___/|_| |_|\__|_| \___/|_|\___|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
; "chance" - fait évoluer un paramètre aléatoirement - comme un lancer de dé
|
||||||
|
; Paramètres :
|
||||||
|
; - Minimum - valeur minimum possible [< Maximum]
|
||||||
|
; - Maximum - valeur maximum possible [> Minimum]
|
||||||
|
chance(1, 5) // Donnera à chaque fois un nombre (décimal) entre 1 et 5 différent
|
||||||
|
|
||||||
|
; "trajectory" - part d'une valeur et évolue progressivement vers une autre
|
||||||
|
; Paramètres :
|
||||||
|
; - Départ - valeur de départ
|
||||||
|
; - Durée - Durée totale de l'évolution (en nombre de temps) [> 0]
|
||||||
|
; - Arrivée - Valeur finale
|
||||||
|
trajectory(0, 10, 1) // évolue de 0 à 1 progressivement en 10 temps
|
||||||
|
|
||||||
|
; "oscillation" - va et vient entre deux valeurs
|
||||||
|
; Paramètres :
|
||||||
|
; - Minimum : valeur minimum [< Maximum]
|
||||||
|
; - Maximum : valeur maximum [> Minimum]
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
oscillation(10, 30, 1/2) // évolue progressivement entre 10 et 30 avec un cycle de 2 temps
|
||||||
|
|
||||||
|
; "alternate" - alterne entre deux valeurs à une certaine vitesse et avec une proportion
|
||||||
|
; Paramètres :
|
||||||
|
; - Valeur 1
|
||||||
|
; - Valeur 2
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
; - Proportion de la valeur 1 [0 - 1] -
|
||||||
|
; par exemple une proportion de 0.75 fait qu'on restera 3/4 du temps sur la valeur 1, et 1/4 du temps sur la valeur 2
|
||||||
|
alternate(50, 55, 1, 0.75) // alterne entre 50 et 55 tous les temps (75% du temps sur valeur 1)
|
||||||
|
|
||||||
|
; "sequence" - lit une suite de valeurs (utile pour les notes et mélodies)
|
||||||
|
; Paramètres :
|
||||||
|
; - liste de valeurs
|
||||||
|
; - Vitesse (par rapport au tempo) [> 0]
|
||||||
|
sequence(array(30, 33, 35), 1)
|
||||||
|
|
||||||
|
; "chaos_control" - génère une valeur qui change en permanence en fonction de l'attracteur
|
||||||
|
; chaotique principal
|
||||||
|
; Paramètres :
|
||||||
|
; - Choix de l'attracteur [1, 2 ou 3]
|
||||||
|
; - Valeur minimum
|
||||||
|
; - Valeur maximum
|
||||||
|
chaos_control(1, 0.5, 1) ; Pour du volume
|
||||||
|
chaos_control(2, 40, 60) ; Pour une note
|
||||||
|
|
||||||
|
|
||||||
|
_
|
||||||
|
| |
|
||||||
|
_____ _____ _ __ ___ _ __ | | ___ ___
|
||||||
|
/ _ \ \/ / _ \ '_ ` _ \| '_ \| |/ _ \/ __|
|
||||||
|
| __/> < __/ | | | | | |_) | | __/\__ \
|
||||||
|
\___/_/\_\___|_| |_| |_| .__/|_|\___||___/
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
|
||||||
|
crunchy(beat(4), 0.5, 10, 1)
|
||||||
|
tempo(0.5)
|
||||||
|
darkwave(beat(1/2), 0.5, 30, 2)
|
||||||
|
darkwave(beat(1/2), 0.5, 33, 2)
|
||||||
|
darkwave(beat(1/4), 0.5, 40, 4)
|
||||||
|
|
||||||
|
chaos_speed(1)
|
||||||
|
|
||||||
|
noisywave(beat(1/4), 0.5, 50, 4)
|
||||||
|
noisywave(beat(1/4), 0.5, 55, 4)
|
||||||
|
|
||||||
|
brightwave(beat(1/2), 0.7, 70, 1)
|
||||||
|
buzzwave(beat(1/2), 0.8, 60, 2, 2)
|
||||||
|
_ _ _ _ _ _ _
|
||||||
|
/\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\
|
||||||
|
\ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' /
|
||||||
|
|_ _|_ _|_ _|_ _|_ _|_ _|_ _|
|
||||||
|
/ , . \ / , . \ / , . \ / , . \ / , . \ / , . \ / , . \
|
||||||
|
\/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/
|
||||||
|
|
||||||
|
/*
|
||||||
|
crunchy(beat(4), 0.6, 10, 1, 2)
|
||||||
|
crunchy(beat(8), 0.6, 50, 1, 2)
|
||||||
|
crunchy(beat(8), 0.6, 55, 1)
|
||||||
|
*/
|
||||||
|
;crunchy(beat(8), 0.8, sequence(array(60, 57, 57, 63, 58), 8), 1)
|
||||||
|
|
||||||
|
tempo(.5)
|
||||||
|
chaos_speed(7)
|
||||||
|
/*
|
||||||
|
brightwave(beat(1/8), 0.5, 50, 3, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 53, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 57, 3, 3)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
glidy(beat(1/2), 0.7, 60, 2)
|
||||||
|
glidy(beat(1/3), 0.7, 64, 2)
|
||||||
|
glidy(beat(1/4), 0.7, 65, 2)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
darkwave(beat(1/4), 0.6, 30, 5, 1)
|
||||||
|
darkwave(beat(1/4), 0.6, 32, 5, 1)
|
||||||
|
darkwave(beat(1/4), 0.6, 27, 5)
|
||||||
|
darkwave(beat(1/4), 0.6, 20, 5)
|
||||||
|
*/
|
||||||
|
|
||||||
|
_ _ _ _ _ _ _
|
||||||
|
/\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\
|
||||||
|
\ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' /
|
||||||
|
|_ _|_ _|_ _|_ _|_ _|_ _|_ _|
|
||||||
|
/ , . \ / , . \ / , . \ / , . \ / , . \ / , . \ / , . \
|
||||||
|
\/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/
|
||||||
|
|
||||||
|
/*
|
||||||
|
darkwave(beat(1/4), .7, 60, 4)
|
||||||
|
darkwave(beat(1/6), .7, 42, 4)
|
||||||
|
darkwave(beat(1/4), .7, 67, 4)
|
||||||
|
darkwave(beat(1/6), .7, 10, 4)
|
||||||
|
|
||||||
|
|
||||||
|
crunchy(beat(4), 0.9, 10, 1, 2)
|
||||||
|
crunchy(beat(8), 0.7, 50, 1, 2)
|
||||||
|
crunchy(beat(8), 0.7, 55, 1)
|
||||||
|
|
||||||
|
crunchy(beat(8), 0.7, sequence(array(60, 57, 57, 63, 58), 8), 1)
|
||||||
|
|
||||||
|
tempo(.125)
|
||||||
|
chaos_speed(8)
|
||||||
|
|
||||||
|
ping(beat(4), 1, sequence(array(50, 55, 50, 57, 50, 58), 1), 1)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
brightwave(beat(1/8), 0.5, 50, 3, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 53, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 57, 3, 3)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
brightwave(beat(1/8), 0.5, 50, 3, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 53, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 57, 3, 3)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
tempo(1)
|
||||||
|
glidy(beat(1/2), 0.3, 60, 2)
|
||||||
|
glidy(beat(1/3), 0.3, 64, 3)
|
||||||
|
glidy(beat(1/4), 0.4, 65, 4)
|
||||||
|
glidy(beat(1/7), 0.5, 40, 5)
|
||||||
|
glidy(beat(1/5), 0.4, 44, 6)
|
||||||
|
glidy(beat(1/6), 0.5, 45, 7)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
noisywave(beat(1/4), 0.9, 20, 6)
|
||||||
|
noisywave(beat(1/4), 0.9, 48, 6)
|
||||||
|
noisywave(beat(1/8), 0.9, 34, 6)
|
||||||
|
noisywave(beat(1/8), 0.9, 39, 6)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ _ _ _ _
|
||||||
|
/\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\
|
||||||
|
\ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' /
|
||||||
|
|_ _|_ _|_ _|_ _|_ _|_ _|_ _|
|
||||||
|
/ , . \ / , . \ / , . \ / , . \ / , . \ / , . \ / , . \
|
||||||
|
\/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tempo(1)
|
||||||
|
chaos_speed(1)
|
||||||
|
darkwave(beat(1/4), .5, 60, 4)
|
||||||
|
darkwave(beat(1/6), .65, 36, 6)
|
||||||
|
darkwave(beat(1/8), .65, 38, 8)
|
||||||
|
darkwave(beat(1/4), .65, 67, 4)
|
||||||
|
darkwave(beat(1/6), .65, 24, 6)
|
||||||
|
darkwave(beat(1/3), .65, 40, 4)
|
||||||
|
darkwave(beat(1/5), .65, 31, 5)
|
||||||
|
|
||||||
|
crunchy(beat(4), 1, 12, 1/2, 2)
|
||||||
|
crunchy(beat(1), 1, 22, 1)
|
||||||
|
crunchy(beat(8), 0.6, 50, 1/2, 2)
|
||||||
|
crunchy(beat(8), 0.6, 55, 1/2)
|
||||||
|
|
||||||
|
crunchy(beat(8), 0.7, sequence(array(60, 57, 57, 63, 58), 8), 1/2)
|
||||||
|
crunchy(beat(8), 0.8, sequence(array(72, 48, 74, 75, 46, 50, 78), 8), 1/2)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
tempo(.125)
|
||||||
|
chaos_speed(8)
|
||||||
|
|
||||||
|
ping(beat(4), 1, sequence(array(50, 55, 50, 57, 50, 58), 1), 1)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
brightwave(beat(1/8), 0.5, 50, 3, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 53, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 57, 3, 3)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
brightwave(beat(1/8), 0.5, 50, 3, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 53, 3)
|
||||||
|
brightwave(beat(1/8), 0.5, 57, 3, 3)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
tempo(1)
|
||||||
|
glidy(beat(1/2), 0.3, 60, 2)
|
||||||
|
glidy(beat(1/3), 0.3, 64, 3)
|
||||||
|
glidy(beat(1/4), 0.4, 65, 4)
|
||||||
|
glidy(beat(1/7), 0.5, 40, 5)
|
||||||
|
glidy(beat(1/5), 0.4, 44, 6)
|
||||||
|
glidy(beat(1/6), 0.5, 45, 7)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
noisywave(beat(1/4), 0.9, 20, 6)
|
||||||
|
noisywave(beat(1/4), 0.9, 48, 6)
|
||||||
|
noisywave(beat(1/8), 0.9, 34, 6)
|
||||||
|
noisywave(beat(1/8), 0.9, 39, 6)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Exemple réalisé en première séance à l'école du Centre
|
||||||
|
crunchy(beat(2), 0.9, chance(15, 80), 0.5)
|
||||||
|
buzzy(beat(1/4), 0.8, 10, 4)
|
||||||
|
tempo(4)
|
||||||
|
glidy(beat(1), 0.6, 60, 1)
|
||||||
|
darkwave(beat(1/4), 0.9, 30, 4)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
ping(beat(8), 1, 10, 1/8)
|
||||||
|
ping(beat(8), 1, sequence(array(20, 22), 1/4), 1/8)
|
||||||
|
crunchy(beat(4), 0.9, chance(30, 80), 1/6)
|
||||||
|
crunchy(beat(4), 0.8, chance(30, 80), 1/6)
|
||||||
|
kick(beat(2), 0.5)
|
||||||
|
snare(beat(1), 0.8, 100, 1/10)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
noisywave(beat(1/4), 0.6, 80, 4)
|
||||||
|
noisywave(beat(1/8), 0.6, 60, 8)
|
||||||
|
noisywave(beat(1/6), 0.6, 40, 6)
|
||||||
|
snare(rhythm(array(1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1), 8), 0.5)
|
||||||
|
kick(rhythm(array(0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0), 8), 0.7)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
; MELODY
|
||||||
|
ping(beat(2), oscillation(.5, 1, 1/2), sequence(array(52, 55, 50, 48, 50)+sequence(array(2, 0), 2), 2), oscillation(.5, 2, 1/2))
|
||||||
|
|
||||||
|
; CHORD
|
||||||
|
buzzwave(beat(4), oscillation(0, .5, 1/6), sequence(array(52, 55, 50, 48, 50)-24, 1/2), 1)
|
||||||
|
buzzwave(beat(4), oscillation(0, .5, 1/3), sequence(array(52, 55, 50, 48, 50)-7, 1/2), 2)
|
||||||
|
|
||||||
|
; BUZZ
|
||||||
|
noisywave(beat(1/4), .5, sequence(array(52, 55), 2), 8)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ _ _ _ _
|
||||||
|
/\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\ /\| |/\
|
||||||
|
\ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' / \ ` ' /
|
||||||
|
|_ _|_ _|_ _|_ _|_ _|_ _|_ _|
|
||||||
|
/ , . \ / , . \ / , . \ / , . \ / , . \ / , . \ / , . \
|
||||||
|
\/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/ \/|_|\/
|
||||||
|
|
||||||
|
; Rythme 1
|
||||||
|
kick(swing(1, 0.75), 0.5)
|
||||||
|
snare(swing(0.5, 0.25), 0.7)
|
||||||
|
hihat( rhythm(array(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1), 4), 0.5, 95, 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
; Ping à corriger
|
||||||
|
; Noisywave avec mode sauvage ou son grave avec mode sauvage aussi
|
||||||
162
public/system-files/archives.orc
Normal file
162
public/system-files/archives.orc
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
|
||||||
|
kick(swing(1, 0.75), 0.5)
|
||||||
|
snare(swing(0.5, 0.25), 0.7)
|
||||||
|
hihat( rhythm(array(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1), 4), 0.3, 95, 0.05)
|
||||||
|
|
||||||
|
tempo(0.5)
|
||||||
|
chaos_speed(0.5)
|
||||||
|
; Base rythmique
|
||||||
|
crunchy(beat(8), 1, 8, 1/4)
|
||||||
|
crunchy(beat(2), 1, 20, 1/2)
|
||||||
|
crunchy(beat(8), 1, trajectory(60, 15, 30), 1/4)
|
||||||
|
crunchy(beat(4), 1, 23, 1/4)
|
||||||
|
|
||||||
|
; Mélodie aléatoire
|
||||||
|
buzzy(beat(8), 0.8, chance(60, 90), 1/16)
|
||||||
|
buzzy(beat(8), 0.8, chance(70, 80), 1/16)
|
||||||
|
buzzy(beat(4), 0.8, trajectory(80, 10, 60), 1/8)
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
ping(beat(1), 0.8, 50,1)
|
||||||
|
ping(beat(1/2), 0.2, 90,2)
|
||||||
|
ping(beat(4), 0.9, 41, 1/3)
|
||||||
|
ping(beat(4), 0.9, 46, 1/4)
|
||||||
|
ping(beat(1/2), 0.8, 37,3)
|
||||||
|
ping(beat(1/4), 0.8, 60,4)
|
||||||
|
ping(beat(1/2), 0.8, 62,2)
|
||||||
|
ping(beat(1/6), 0.8, 67,6)
|
||||||
|
|
||||||
|
buzzwave(beat(1/3), 0.8, sequence(array(20, 25), 1), 3)
|
||||||
|
|
||||||
|
tempo(1)
|
||||||
|
snare(swing(2, 0.25), 0.9)
|
||||||
|
kick(swing(0.5, 0.75), 0.5)
|
||||||
|
hihat( swing(4, 0.25), 0.6, chance(80, 100), 0.1)
|
||||||
|
hihat( beat(8), 0.8, 100, 0.05)
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; Lunégroove
|
||||||
|
noisywave(beat(1/4), 0.9, sequence(array(20, 30, 23, 27), 1/4), 4)
|
||||||
|
noisywave(beat(1/8), 0.9, sequence(array(60, 60, 65, 53), 1/8), 8)
|
||||||
|
noisywave(beat(1/6), 0.9, sequence(array(40, 43, 46), 1/6), 6)
|
||||||
|
glidy(rhythm(array(1, 1, 1, 0, 0, 1), 2), 0.5, sequence(array(70, 75, 53, 51), 3), 1/4)
|
||||||
|
glidy(rhythm(array(0, 1, 0, 1, 1, 1), 2), 0.5, sequence(array(73, 65, 60), 3), 1/2)
|
||||||
|
brightwave(beat(1/4), 1, sequence(array(15, 18, 23), 1/4), 7)
|
||||||
|
|
||||||
|
snare(rhythm(array(1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1), 4), 0.5)
|
||||||
|
kick(rhythm(array(0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0), 2), 0.7)
|
||||||
|
tempo(1)
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; LA VOITURE EN PANNE !!!!!!
|
||||||
|
|
||||||
|
tempo(1.5)
|
||||||
|
kick(rhythm(array(1, 0, 0, 0, 0, 1, 0, 0), 2), 0.45)
|
||||||
|
snare(rhythm(array(0, 0, 1, 0, 0, 0, 1, 0), 2), 0.25)
|
||||||
|
hihat(beat(4), 0.15, 95, 1/12)
|
||||||
|
|
||||||
|
ping(beat(2), 0.45, sequence(array(20, 21), 1/8), .5)
|
||||||
|
ping(beat(1/4), .95, sequence(array(20, 21)-12, 1/8), 4)
|
||||||
|
|
||||||
|
crunchy(beat(1), .95, sequence(array(20, 18, 28, 21), 1/7)+12*sequence(array(1, 2, 3), 1), 1+sequence(array(1, 2, 3), 1))
|
||||||
|
|
||||||
|
brightwave(swing(1/8, .95), 0.55, sequence(array(21, 20, 25, 30)+24, 1/8), 6)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; on va faire fortune dans le reggae
|
||||||
|
|
||||||
|
tempo(1)
|
||||||
|
crunchy(rhythm(array(1, 1, 1, 1, 1, 1, 0, 0), 8), 0.8, sequence(array(11, 17), 1/2), 1/4)
|
||||||
|
|
||||||
|
ping(rhythm(array(0, 1), 2), 0.9, sequence(array(43, 41), 1/2), 0.25 )
|
||||||
|
ping(rhythm(array(0, 1), 2), 0.9, sequence(array(47, 45), 1/2), 0.25)
|
||||||
|
ping(rhythm(array(0, 1), 2), 0.9, sequence(array(50, 48), 1/2), 0.25)
|
||||||
|
|
||||||
|
glidy(beat(1/4), 0.2, sequence(array(49, 55, 52, 67, 46, 70), 1/4), 6)
|
||||||
|
|
||||||
|
kick(rhythm(array(1, 0), 2), 0.8)
|
||||||
|
snare(beat(1), 0.6)
|
||||||
|
hihat(rhythm(array(1, 1, 1, 0, 1, 0), 6), 0.7, 90, 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; Ardoise
|
||||||
|
tempo(1.25)
|
||||||
|
kick(rhythm(array(1, 1, 0, 0, 0, 1), 4), .5)
|
||||||
|
snare(beat(1/2), .5)
|
||||||
|
hihat(rhythm(array(0, 1, 1, 0, 1, 1, 1), 4), .25, 95-4*swing(1, .25), 1/8)
|
||||||
|
|
||||||
|
ping(beat(6), .35, sequence(array(48, 51, 55, 60, 48, 51, 55, 60, 47, 51, 55, 60, 44, 51, 55, 60), oscillation(6, 12, 1/32)), oscillation(.25, 3, 1/8))
|
||||||
|
ping(beat(6), .35, sequence(array(48, 51, 55, 60, 48, 51, 55, 60, 47, 51, 55, 60, 44, 51, 55, 60), oscillation(2, 16, 1/24)), oscillation(.25, 3, 1/3))
|
||||||
|
darkwave(beat(3), .85, sequence(array(48, 51, 55, 60, 47, 51, 55, 60, 44, 51, 55, 60)-24, oscillation(3, 9, 1/48)), oscillation(1, 3, 1/3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;KS
|
||||||
|
chaos_speed(1)
|
||||||
|
$tempo = .5
|
||||||
|
;ks(beat(1), 0.8, 44, .5)
|
||||||
|
ks(beat(.25), 0.8, 16, .5)
|
||||||
|
ks(beat(1), 0.9, 52, .5)
|
||||||
|
ks(beat(3), 0.9, 64, 1)
|
||||||
|
ks(beat(4), 0.9, 83, 2)
|
||||||
|
;ks(beat(8), 0.5, sequence(array(75, 83, 80, 85, 83), 1), .25)
|
||||||
|
;ks(beat( alternate(.5, 2, .5, 0.5) ), 0.8, 30, 1)
|
||||||
|
|
||||||
|
;ping(beat(4), 0.8, 72, .8)
|
||||||
|
;crunchy(beat(16), 0.6, trajectory(16, 4, 32), .5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;MOBILE 1
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
$tempo = 0.25
|
||||||
|
klf = abs(lfo:k(1, 0.05))
|
||||||
|
kilf = 1 - klf
|
||||||
|
strike(beat(1/16), 0.9 * klf , alternate(40, 28, 1/4, 0.5), 16)
|
||||||
|
strike(beat(1/16), 0.9 * klf , alternate(6, 18, 1/4, 0.5), 16)
|
||||||
|
strike(beat(1/12), 0.9 * kilf , alternate(30, 18, 1/8, 0.5), 12)
|
||||||
|
|
||||||
|
kmet = drunk(0.125, 0.25)
|
||||||
|
ktog init 0
|
||||||
|
krhythm_mult init 1
|
||||||
|
if(kmet == 1 ) then
|
||||||
|
ktog = 1 - ktog
|
||||||
|
if(rint:k(1, 6) == 1) then
|
||||||
|
krhythm_mult = rint:k(1, 3)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
if(ktog == 1) then
|
||||||
|
strike( rhythm(array(1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1), 4 * krhythm_mult),
|
||||||
|
oscillation(0.1, 0.6, 10),
|
||||||
|
sequence(array(60, 64, 66, 67, 69), 8) +
|
||||||
|
sequence(array(2, 3, 8, 1, 5, 10), 3.3), 1/2)
|
||||||
|
else
|
||||||
|
buzzy(beat(krhythm_mult * 2), 0.9, oscillation(40, 70, 10), 1)
|
||||||
|
buzzy(beat(1/2), 0.8 * klf , alternate(44, 33, 1/4, 0.5), 2)
|
||||||
|
buzzy(beat(1/3), 0.8 * kilf , alternate(24, 21, 1/8, 0.5), 2)
|
||||||
|
endif
|
||||||
|
|
||||||
|
smooth(drunk(1/10, 0.3), 0.5, sequence(array(60, 30, 64, 67), 1), 8)
|
||||||
|
smooth(drunk(1/9, 0.3), 0.5, sequence(array(48, 18, 66, 70, 65), 1/2), 8)
|
||||||
9
public/system-files/examples.orc
Normal file
9
public/system-files/examples.orc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░░▒▓██████████████▓▒░░▒▓███████▓▒░░▒▓█▓▒░ ░▒▓████████▓▒░░▒▓███████▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
|
||||||
|
░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
|
||||||
|
░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓████████▓▒░▒▓████████▓▒░▒▓███████▓▒░
|
||||||
|
|
||||||
|
|
||||||
159
public/system-files/globals.orc
Normal file
159
public/system-files/globals.orc
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
gktempo init 1
|
||||||
|
gkphasor init 0
|
||||||
|
gkslowph init 0
|
||||||
|
gkbeat init 0
|
||||||
|
gkgain init 0.05
|
||||||
|
#define tempo #gktempo#
|
||||||
|
|
||||||
|
instr metronome_instr
|
||||||
|
kenv = expseg(1.01, p3, 0.01) - 0.01
|
||||||
|
asig = oscili(0.3, 3000) * kenv
|
||||||
|
chnmix(asig, "out1")
|
||||||
|
chnmix(asig, "out2")
|
||||||
|
endin
|
||||||
|
|
||||||
|
opcode metronome, 0, 0
|
||||||
|
kmet = gkbeat
|
||||||
|
if(kmet == 1) then
|
||||||
|
schedulek("metronome_instr", 0, 0.1)
|
||||||
|
endif
|
||||||
|
endop
|
||||||
|
|
||||||
|
instr gclock
|
||||||
|
gkphasor = phasor:k(gktempo)
|
||||||
|
gkslowph = phasor:k(gktempo / 100)
|
||||||
|
gkbeat = metro:k(gktempo)
|
||||||
|
endin
|
||||||
|
schedule("gclock", 0, -1)
|
||||||
|
|
||||||
|
opcode reset_clock, 0, 0
|
||||||
|
turnoff2( nstrnum("gclock"), 0, 0)
|
||||||
|
schedule("gclock", 0.01, -1)
|
||||||
|
endop
|
||||||
|
|
||||||
|
instr audio_out
|
||||||
|
|
||||||
|
a1 = clip:a(chnget:a("out1"), 0, 0.99);limit:a(chnget:a("out1"), -1, 1) ;* gkgain
|
||||||
|
a2 = clip:a(chnget:a("out2"), 0, 0.99);limit:a(chnget:a("out2"), -1, 1) ;* gkgain
|
||||||
|
chnclear("out1")
|
||||||
|
chnclear("out2")
|
||||||
|
outs a1, a2
|
||||||
|
endin
|
||||||
|
schedule("audio_out", 0, -1)
|
||||||
|
|
||||||
|
|
||||||
|
gklorenz_speed init 0.00001
|
||||||
|
|
||||||
|
gklorenz_x init 0
|
||||||
|
gklorenz_y init 0
|
||||||
|
gklorenz_z init 0
|
||||||
|
gkmirlorenz_x init 0
|
||||||
|
gkmirlorenz_y init 0
|
||||||
|
gkmirlorenz_z init 0
|
||||||
|
galorenz_x init 0
|
||||||
|
galorenz_y init 0
|
||||||
|
galorenz_z init 0
|
||||||
|
|
||||||
|
opcode lorenz_ctl, aaa, kppp
|
||||||
|
kspeed, ix, iy, iz xin
|
||||||
|
kspeed = limit:k(kspeed, 0.0000001, 0.09)
|
||||||
|
ksv init 10
|
||||||
|
krv init 28
|
||||||
|
kbv init 2.6
|
||||||
|
; kh init 0.001
|
||||||
|
kh = kspeed
|
||||||
|
ix = (ix != 0) ? ix : 0.6
|
||||||
|
iy = (iy != 0) ? iy : 0.6
|
||||||
|
iz = (iz != 0) ? iz : 0.6
|
||||||
|
iskip = 1
|
||||||
|
ax1, ay1, az1 lorenz ksv, krv, kbv, kh, ix, iy, iz, iskip
|
||||||
|
|
||||||
|
ax1 /= krv
|
||||||
|
ay1 /= krv
|
||||||
|
az1 /= krv
|
||||||
|
|
||||||
|
xout ax1, ay1, az1
|
||||||
|
endop
|
||||||
|
|
||||||
|
#define LORENZ_TIME #720#
|
||||||
|
|
||||||
|
instr lorenz_sched
|
||||||
|
ix = random:i(0, 1)
|
||||||
|
iy = random:i(0, 1)
|
||||||
|
iz = random:i(0, 1)
|
||||||
|
ax, ay, az lorenz_ctl gklorenz_speed, ix, iy, iz
|
||||||
|
gklorenz_x = lag(k(ax), 0.01)
|
||||||
|
gklorenz_y = lag(k(ay), 0.01)
|
||||||
|
gklorenz_z = lag(k(az), 0.01)
|
||||||
|
gkmirlorenz_x = mirror:k(gklorenz_x, 0, 1)
|
||||||
|
gkmirlorenz_y = mirror:k(gklorenz_y, 0, 1)
|
||||||
|
gkmirlorenz_z = mirror:k(gklorenz_z, 0, 1)
|
||||||
|
galorenz_x = ax
|
||||||
|
galorenz_y = ay
|
||||||
|
galorenz_z = az
|
||||||
|
schedule("lorenz_sched", $LORENZ_TIME, $LORENZ_TIME)
|
||||||
|
endin
|
||||||
|
|
||||||
|
schedule("lorenz_sched", 0, $LORENZ_TIME)
|
||||||
|
|
||||||
|
#define FQ #
|
||||||
|
isavage_mode init p6
|
||||||
|
ifq mtof inote
|
||||||
|
|
||||||
|
ksavage init 0
|
||||||
|
if(isavage_mode == 1) then
|
||||||
|
ksavage = savage_1()
|
||||||
|
elseif(isavage_mode == 2) then
|
||||||
|
ksavage = savage_2()
|
||||||
|
elseif(isavage_mode == 3) then
|
||||||
|
ksavage = savage_3()
|
||||||
|
elseif(isavage_mode == 4) then
|
||||||
|
ksavage = savage_4()
|
||||||
|
elseif(isavage_mode == 5) then
|
||||||
|
ksavage = savage_5()
|
||||||
|
endif
|
||||||
|
|
||||||
|
kfq = limit:k(ifq + (ksavage * ifq), 20, 20000)
|
||||||
|
#
|
||||||
|
|
||||||
|
#define FQPERC #
|
||||||
|
isavage_mode = rint:i(1, 5)
|
||||||
|
ksavage init 0
|
||||||
|
if(isavage_mode == 1) then
|
||||||
|
ksavage = savage_1()
|
||||||
|
elseif(isavage_mode == 2) then
|
||||||
|
ksavage = savage_2()
|
||||||
|
elseif(isavage_mode == 3) then
|
||||||
|
ksavage = savage_3()
|
||||||
|
elseif(isavage_mode == 4) then
|
||||||
|
ksavage = savage_4()
|
||||||
|
elseif(isavage_mode == 5) then
|
||||||
|
ksavage = savage_5()
|
||||||
|
endif
|
||||||
|
#
|
||||||
|
|
||||||
|
#define SAVAGE(isavage_mode') #
|
||||||
|
ksavage init 0
|
||||||
|
if(isavage_mode == 1) then
|
||||||
|
ksavage = savage_1()
|
||||||
|
elseif(isavage_mode == 2) then
|
||||||
|
ksavage = savage_2()
|
||||||
|
elseif(isavage_mode == 3) then
|
||||||
|
ksavage = savage_3()
|
||||||
|
elseif(isavage_mode == 4) then
|
||||||
|
ksavage = savage_4()
|
||||||
|
elseif(isavage_mode == 5) then
|
||||||
|
ksavage = savage_5()
|
||||||
|
endif
|
||||||
|
#
|
||||||
|
|
||||||
|
opcode tempo, 0, k
|
||||||
|
kval xin
|
||||||
|
$tempo = limit:k(kval, 0.001, 10)
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode chaos_speed, 0, k
|
||||||
|
kval xin
|
||||||
|
gklorenz_speed = 0.0000001 * (limit:k(kval, 0.01, 100) * 10)
|
||||||
|
endop
|
||||||
|
|
||||||
414
public/system-files/lib.orc
Normal file
414
public/system-files/lib.orc
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
; instruments management
|
||||||
|
instr KillImpl
|
||||||
|
Sinstr = p4
|
||||||
|
if (nstrnum(Sinstr) > 0) then
|
||||||
|
turnoff2(Sinstr, 0, 0)
|
||||||
|
endif
|
||||||
|
turnoff
|
||||||
|
endin
|
||||||
|
|
||||||
|
opcode kill, 0, S
|
||||||
|
Sinstr xin
|
||||||
|
schedule("KillImpl", 0, .05, Sinstr)
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode refresh, 0, S
|
||||||
|
Sinstr xin
|
||||||
|
if (nstrnum(Sinstr) > 0) then
|
||||||
|
kill(Sinstr)
|
||||||
|
schedule(Sinstr, ksmps / sr, -1)
|
||||||
|
endif
|
||||||
|
endop
|
||||||
|
|
||||||
|
#define _endin #
|
||||||
|
endin
|
||||||
|
refresh("livecoding")
|
||||||
|
#
|
||||||
|
|
||||||
|
opcode rint, i, ii
|
||||||
|
imin, imax xin
|
||||||
|
irnd = int(random:i(imin, int(imax) + 0.99))
|
||||||
|
xout irnd
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode rint, k, kk
|
||||||
|
kmin, kmax xin
|
||||||
|
krnd = int(random:k(kmin, int(kmax) + 0.99))
|
||||||
|
xout krnd
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode euclidian, k, kkkk
|
||||||
|
konset, kdiv, kpulses, krot xin
|
||||||
|
kphasor = gkphasor
|
||||||
|
kph = int( ( ( (kphasor + krot) * kdiv) / 1) * kpulses)
|
||||||
|
keucval = int((konset / kpulses) * kph)
|
||||||
|
kold_euc init i(keucval)
|
||||||
|
kold_ph init i(kph)
|
||||||
|
kres = ((kold_euc != keucval) && (kold_ph != kph)) ? 1 : 0
|
||||||
|
kold_euc = keucval
|
||||||
|
kold_ph = kph
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
// Simple rhythm array
|
||||||
|
opcode array_rhythm, k, k[]
|
||||||
|
karr[] xin
|
||||||
|
kmet = gkbeat
|
||||||
|
kcnt init 0
|
||||||
|
ilen = lenarray(karr)
|
||||||
|
ktrig = 0
|
||||||
|
if( kmet > 0 ) then
|
||||||
|
kval = karr[kcnt]
|
||||||
|
ktrig = kval
|
||||||
|
kcnt = (kcnt + 1) % ilen
|
||||||
|
endif
|
||||||
|
|
||||||
|
xout ktrig
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode trunc, k, k
|
||||||
|
kx xin
|
||||||
|
ky = (kx < 0) ? -(floor(-kx)) : floor(kx)
|
||||||
|
xout ky
|
||||||
|
endop
|
||||||
|
opcode mfmod, k, kk
|
||||||
|
kone, ktwo xin
|
||||||
|
kres = kone - trunc(kone/ktwo) * ktwo
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode phase_trig, k, k
|
||||||
|
kphase xin
|
||||||
|
kold init 1
|
||||||
|
ktrigger init 0
|
||||||
|
ktrigger = 0
|
||||||
|
if(kold > kphase) then
|
||||||
|
ktrigger = 1
|
||||||
|
endif
|
||||||
|
kold = kphase
|
||||||
|
xout ktrigger
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode downprint, 0, kki
|
||||||
|
kprint, kdown, imargin xin
|
||||||
|
kcnt init 0
|
||||||
|
if(kcnt == 0) then
|
||||||
|
printk2(kprint, imargin)
|
||||||
|
endif
|
||||||
|
kcnt = (kcnt + 1) % kdown
|
||||||
|
endop
|
||||||
|
|
||||||
|
|
||||||
|
opcode phasor_fqmult, k, kk
|
||||||
|
kphase, kmult xin
|
||||||
|
if(kmult <= 0) then
|
||||||
|
kmult = 1
|
||||||
|
endif
|
||||||
|
kfphase init 0
|
||||||
|
if(kmult <= 1) then
|
||||||
|
kfphase = mfmod(gkslowph * (kmult * 100), 1)
|
||||||
|
elseif(kmult > 1) then
|
||||||
|
kfphase = mfmod(gkslowph * (kmult * 100), 1)
|
||||||
|
;kfphase= mfmod(gkphasor * kmult, 1)
|
||||||
|
endif
|
||||||
|
xout kfphase
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode rh_subd, k, k[]P
|
||||||
|
karr[], kpdiv xin
|
||||||
|
ilen = lenarray(karr)
|
||||||
|
if(kpdiv <= 0) then
|
||||||
|
kpdiv = ilen
|
||||||
|
endif
|
||||||
|
kdiv = kpdiv
|
||||||
|
kph = phasor_fqmult(gkphasor, kdiv )
|
||||||
|
ktr = phase_trig(kph)
|
||||||
|
kidx init 0
|
||||||
|
ktrigger = 0
|
||||||
|
if(ktr == 1) then
|
||||||
|
ktrigger = karr[kidx]
|
||||||
|
kidx = (kidx+1) % ilen
|
||||||
|
endif
|
||||||
|
xout ktrigger
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode rhythm, k, k[]P
|
||||||
|
karr[], kpdiv xin
|
||||||
|
ilen = lenarray(karr)
|
||||||
|
if(kpdiv <= 0) then
|
||||||
|
kpdiv = ilen
|
||||||
|
endif
|
||||||
|
kdiv = kpdiv
|
||||||
|
kph = phasor_fqmult(gkphasor, kdiv)
|
||||||
|
ktr = phase_trig(kph)
|
||||||
|
kidx init 0
|
||||||
|
ktrigger = 0
|
||||||
|
if(ktr == 1) then
|
||||||
|
ktrigger = karr[kidx]
|
||||||
|
kidx = (kidx+1) % ilen
|
||||||
|
endif
|
||||||
|
xout ktrigger
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode beat, k, k
|
||||||
|
kmult xin
|
||||||
|
kmult = limit:k(kmult, 0.001, 1000)
|
||||||
|
km = rhythm(array(1), kmult)
|
||||||
|
;kph = phasor:k(kmult * gktempo)
|
||||||
|
;km = phase_trig(kph)
|
||||||
|
xout km
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode swing, k, kk
|
||||||
|
kspeed, ksw xin
|
||||||
|
ktrig init 0
|
||||||
|
kold init 0
|
||||||
|
kph = phasor_fqmult(gkphasor, kspeed)
|
||||||
|
ktr1 = phase_trig(kph)
|
||||||
|
ktr2 = 0
|
||||||
|
if(kold < ksw && kph >= ksw) then
|
||||||
|
ktr2 = 0.6
|
||||||
|
endif
|
||||||
|
kold = kph
|
||||||
|
ktrig = limit:k(ktr1 + ktr2, 0, 1)
|
||||||
|
xout ktrig
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode drunk, k, kk
|
||||||
|
kspeed, kdrunk xin
|
||||||
|
kold init 0
|
||||||
|
kspeed = limit:k(kspeed, 0.001, 1000)
|
||||||
|
knoi = random:k(0, kdrunk)
|
||||||
|
|
||||||
|
kph = phasor_fqmult(gkphasor, kspeed)
|
||||||
|
;kph = phase_incr_mod(gkphasor, 1/ kspeed)
|
||||||
|
ktrig = limit:k(phase_trig(kph) + ((kold < knoi && kph > knoi) ? 1 : 0), 0, 1)
|
||||||
|
|
||||||
|
kold = kph
|
||||||
|
xout ktrig
|
||||||
|
endop
|
||||||
|
|
||||||
|
|
||||||
|
opcode filter_first_trig, k, k
|
||||||
|
ktrig xin
|
||||||
|
ksub init 1
|
||||||
|
ktrig -= ksub
|
||||||
|
ksub = 0
|
||||||
|
xout ktrig
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode check_trig, k, k
|
||||||
|
ktrig xin
|
||||||
|
kf = filter_first_trig(ktrig)
|
||||||
|
kres = (changed:k(kf) > 0 && kf > 0) ? 1 : 0
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode to_tempo_dur, k, k
|
||||||
|
kdur xin
|
||||||
|
ktdur = kdur / gktempo
|
||||||
|
xout ktdur
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode chance, k, kk
|
||||||
|
kmin, kmax xin
|
||||||
|
kres = random:k(kmin, kmax)
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode alternate, k, kkkO
|
||||||
|
kone, ktwo, kspeed, kduty xin
|
||||||
|
|
||||||
|
if(kduty == 0) then
|
||||||
|
kduty = 0.5
|
||||||
|
endif
|
||||||
|
if(kspeed <= 0) then
|
||||||
|
kspeed = 1
|
||||||
|
endif
|
||||||
|
kspeed = limit:k(to_tempo_dur(kspeed), 0.0001, 1000)
|
||||||
|
kph = phasor_fqmult(gkphasor, kspeed)
|
||||||
|
kres init i(kone)
|
||||||
|
if(kph < kduty) then
|
||||||
|
kres = kone
|
||||||
|
else
|
||||||
|
kres = ktwo
|
||||||
|
endif
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode trajectory, k, kkk
|
||||||
|
kfrom, kdur, kto xin
|
||||||
|
kdur = to_tempo_dur(kdur)
|
||||||
|
kres = linseg:k( i(kfrom), i(kdur), i(kto) )
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode oscillation, k, kkk
|
||||||
|
kmin, kmax, kspeed xin
|
||||||
|
if(kmin > kmax) then
|
||||||
|
ktmp = kmin
|
||||||
|
kmin = kmax
|
||||||
|
kmax = ktmp
|
||||||
|
endif
|
||||||
|
kspeed = limit:k(gktempo / kspeed, 0.001, 1000)
|
||||||
|
|
||||||
|
kfq = kspeed ; / gktempo
|
||||||
|
iphs init i(gkphasor)
|
||||||
|
kosc = oscili(0.5, kfq, -1, iphs) + 0.5
|
||||||
|
kres = kosc * (kmax - kmin) + kmin
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode sequence, k, k[]P
|
||||||
|
karr[], kpdiv xin
|
||||||
|
ilen = lenarray(karr)
|
||||||
|
if(kpdiv <= 0) then
|
||||||
|
kpdiv = ilen
|
||||||
|
endif
|
||||||
|
kdiv = kpdiv
|
||||||
|
kph = phasor_fqmult(gkphasor, kdiv )
|
||||||
|
ktr = phase_trig(kph)
|
||||||
|
kidx init 0
|
||||||
|
if(ktr == 1) then
|
||||||
|
kidx = (kidx+1) % ilen
|
||||||
|
endif
|
||||||
|
ktrigger = karr[kidx]
|
||||||
|
xout ktrigger
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode random_array_int, i[], iii
|
||||||
|
isize, imin, imax xin
|
||||||
|
iarr[] init isize
|
||||||
|
|
||||||
|
icnt init 0
|
||||||
|
while icnt < isize do
|
||||||
|
iarr[icnt] = int(random:i(imin, imax+0.99))
|
||||||
|
icnt += 1
|
||||||
|
od
|
||||||
|
xout iarr
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode random_array_int, k[], iii
|
||||||
|
isize, imin, imax xin
|
||||||
|
kArr[] init isize
|
||||||
|
icnt init 0
|
||||||
|
iArr[] = random_array_int(isize, imin, imax)
|
||||||
|
while icnt < isize do
|
||||||
|
kArr[icnt] = iArr[icnt]
|
||||||
|
icnt += 1
|
||||||
|
od
|
||||||
|
xout kArr
|
||||||
|
endop
|
||||||
|
|
||||||
|
#define arr #fillarray(#
|
||||||
|
|
||||||
|
; kind of random based savage
|
||||||
|
opcode savage_1, k, 0
|
||||||
|
k1 = gkmirlorenz_x
|
||||||
|
k2 = abs(oscili:k(1, gkmirlorenz_y * 6 + 0.01))
|
||||||
|
k3 = rspline:k(0, gkmirlorenz_z, 1, 5)
|
||||||
|
|
||||||
|
kfq = gkmirlorenz_x * rspline:k(1, 3, 1, 5) + 0.001
|
||||||
|
kmod = beat(kfq) ;lfo:k(1, kfq, 3)
|
||||||
|
|
||||||
|
ksel init 0
|
||||||
|
ksel_sig init 0
|
||||||
|
if(trigger:k(kmod, 0.5, 0) == 1) then
|
||||||
|
ksel = rint:k(0, 2)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if(ksel == 0) then
|
||||||
|
ksel_sig = k1
|
||||||
|
elseif(ksel == 1) then
|
||||||
|
ksel_sig = k2
|
||||||
|
elseif(ksel == 2) then
|
||||||
|
ksel_sig = k3
|
||||||
|
endif
|
||||||
|
|
||||||
|
kres = (kmod > 0) ? ksel_sig : 0
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
; direct lorenz savage mode
|
||||||
|
opcode savage_2, k, 0
|
||||||
|
kmult = pow(abs(lfo:k(1, rspline:k(0, gklorenz_x, 0.1, 3), 1)),3)
|
||||||
|
kmod = abs(poscil:k( pow(gkmirlorenz_z, 3) * kmult, gkmirlorenz_x * gkmirlorenz_y * 40))
|
||||||
|
xout kmod
|
||||||
|
endop
|
||||||
|
|
||||||
|
; pwm based savage mode
|
||||||
|
opcode savage_3, k, 0
|
||||||
|
kduty = gkmirlorenz_y
|
||||||
|
kph = phasor:k(gkmirlorenz_x * 5 )
|
||||||
|
kres init 0
|
||||||
|
if(kph < kduty) then
|
||||||
|
kres = 1
|
||||||
|
else
|
||||||
|
kres = 0
|
||||||
|
endif
|
||||||
|
|
||||||
|
kmult = mirror:k(gklorenz_z, 0, 1)
|
||||||
|
kres *= kmult
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode savage_4, k, 0
|
||||||
|
iarr[] = fillarray(random:i(0, 1), random:i(0, 1), random:i(0, 1), random:i(0, 1))
|
||||||
|
|
||||||
|
kchange init 1
|
||||||
|
kchange = changed:k(round(abs(gklorenz_x )))
|
||||||
|
kmod init 1
|
||||||
|
if(kchange == 1) then
|
||||||
|
iarr[0] = random:i(0, 1)
|
||||||
|
iarr[1] = random:i(0, 1)
|
||||||
|
iarr[2] = random:i(0, 1)
|
||||||
|
iarr[3] = random:i(0, 1)
|
||||||
|
endif
|
||||||
|
ksel = int(abs(gklorenz_x) * 4.99) % 4
|
||||||
|
kres = limit:k (lineto(iarr[ksel], mirror:k(abs(gklorenz_y), 0, 1) ), 0, 1)
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode savage_5, k, 0
|
||||||
|
kamp = pow(abs(gklorenz_x), 2)
|
||||||
|
kfq = 8 - (0.01 + abs(gklorenz_y) * 8)
|
||||||
|
kmod = oscili:k(kamp, kfq)
|
||||||
|
xout kmod
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode chaos_rhythm, k, kk
|
||||||
|
kchoice, kthresh xin
|
||||||
|
kthresh = limit:k(kthresh, 0, 1)
|
||||||
|
kchoice = int(limit:k(kchoice, 1, 3.99))
|
||||||
|
kval init 0
|
||||||
|
if(kchoice == 1) then
|
||||||
|
kval = gkmirlorenz_x
|
||||||
|
elseif(kchoice == 2) then
|
||||||
|
kval = gkmirlorenz_y
|
||||||
|
elseif(kchoice == 3) then
|
||||||
|
kval = gkmirlorenz_z
|
||||||
|
endif
|
||||||
|
|
||||||
|
kres = trigger:k(kval, kthresh, 2)
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode chaos_control, k, kkk
|
||||||
|
kchoice, kmin, kmax xin
|
||||||
|
kmax = max:k(kmin, kmax)
|
||||||
|
kmin = min:k(kmin, kmax)
|
||||||
|
kchoice = int(limit:k(kchoice, 1, 3.99))
|
||||||
|
kval init 0
|
||||||
|
if(kchoice == 1) then
|
||||||
|
kval = gkmirlorenz_x
|
||||||
|
elseif(kchoice == 2) then
|
||||||
|
kval = gkmirlorenz_y
|
||||||
|
elseif(kchoice == 3) then
|
||||||
|
kval = gkmirlorenz_z
|
||||||
|
endif
|
||||||
|
kdiff = kmax - kmin
|
||||||
|
kres = kval * kdiff + kmin
|
||||||
|
xout kres
|
||||||
|
endop
|
||||||
|
|
||||||
|
|
||||||
3
public/system-files/livecode.orc
Normal file
3
public/system-files/livecode.orc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
instr livecoding
|
||||||
|
|
||||||
|
$_endin
|
||||||
207
public/system-files/scale.orc
Normal file
207
public/system-files/scale.orc
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
gi3semitone ftgen 0, 0, 5, -2, 0, 3, 6, 9, 12
|
||||||
|
gi4semitone ftgen 0, 0, 4, -2, 0, 4, 8, 12
|
||||||
|
giaeolian ftgen 0, 0, 7, -2, 0, 2, 3, 5, 7, 8, 10
|
||||||
|
giaeolian ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 8, 10, 12
|
||||||
|
gialgerian ftgen 0, 0, 8, -2, 0, 2, 3, 6, 7, 8, 11, 12
|
||||||
|
gialgerian1 ftgen 0, 0, 8, -2, 0, 2, 3, 6, 7, 8, 11, 12
|
||||||
|
gialgerian2 ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 8, 10, 12
|
||||||
|
gialtered ftgen 0, 0, 8, -2, 0, 1, 3, 4, 6, 8, 10, 12
|
||||||
|
giarabian ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 11, 12
|
||||||
|
giaugmented ftgen 0, 0, 7, -2, 0, 3, 4, 7, 8, 11, 12
|
||||||
|
gibalinese ftgen 0, 0, 6, -2, 0, 1, 3, 7, 8, 12
|
||||||
|
gibebopdominant ftgen 0, 0, 9, -2, 0, 2, 4, 5, 7, 9, 10, 11, 12
|
||||||
|
gibebopdominantflatnine ftgen 0, 0, 9, -2, 0, 1, 4, 5, 7, 9, 10, 11, 12
|
||||||
|
gibebopmajor ftgen 0, 0, 9, -2, 0, 2, 4, 5, 7, 8, 9, 11, 12
|
||||||
|
gibebopminor ftgen 0, 0, 9, -2, 0, 2, 3, 5, 7, 8, 9, 10, 12
|
||||||
|
gibeboptonicminor ftgen 0, 0, 9, -2, 0, 2, 3, 5, 7, 8, 9, 11, 12
|
||||||
|
giblues ftgen 0, 0, 7, -2, 0, 3, 5, 6, 7, 10, 12
|
||||||
|
gibyzantine ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 11, 12
|
||||||
|
gichahargah ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 11, 12
|
||||||
|
gichinese ftgen 0, 0, 6, -2, 0, 2, 4, 7, 9, 12
|
||||||
|
gichinese2 ftgen 0, 0, 6, -2, 0, 4, 6, 7, 11, 12
|
||||||
|
gichroma ftgen 0, 0, 12, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
|
||||||
|
gichromatic ftgen 0, 0, 13, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
|
||||||
|
gidim ftgen 0, 0, 4, -2, 0, 3, 6, 9
|
||||||
|
gidiminished ftgen 0, 0, 9, -2, 0, 2, 3, 5, 6, 8, 9, 11, 12
|
||||||
|
gidorian ftgen 0, 0, 7, -2, 0, 2, 3, 5, 7, 9, 10
|
||||||
|
gidorian ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 9, 10, 12
|
||||||
|
gidoubleharmonic ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 11, 12
|
||||||
|
giegyptian ftgen 0, 0, 8, -2, 0, 2, 3, 6, 7, 8, 11, 12
|
||||||
|
gienigmatic ftgen 0, 0, 8, -2, 0, 1, 4, 6, 8, 10, 11, 12
|
||||||
|
giethiopian ftgen 0, 0, 8, -2, 0, 2, 4, 5, 7, 8, 11, 12
|
||||||
|
giflamenco ftgen 0, 0, 9, -2, 0, 1, 3, 4, 5, 7, 8, 10, 12
|
||||||
|
gigypsy ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 11, 12
|
||||||
|
giharmonic ftgen 0, 0, 7, -2, 0, 2, 4, 5, 7, 9, 11
|
||||||
|
giharmonicmajor ftgen 0, 0, 8, -2, 0, 2, 4, 5, 8, 9, 11, 12
|
||||||
|
giharmonicminor ftgen 0, 0, 7, -2, 0, 2, 3, 5, 7, 8, 11
|
||||||
|
giharmonicminor ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 8, 11, 12
|
||||||
|
gihindu ftgen 0, 0, 8, -2, 0, 2, 4, 5, 7, 8, 10, 12
|
||||||
|
gihirajoshi ftgen 0, 0, 6, -2, 0, 2, 3, 7, 8, 12
|
||||||
|
gihungariangypsy ftgen 0, 0, 8, -2, 0, 2, 3, 6, 7, 8, 11, 12
|
||||||
|
gihungarianmajor ftgen 0, 0, 8, -2, 0, 3, 4, 6, 7, 9, 10, 12
|
||||||
|
gihungarianminor ftgen 0, 0, 8, -2, 0, 2, 3, 6, 7, 8, 11, 12
|
||||||
|
giindian ftgen 0, 0, 8, -2, 0, 1, 3, 4, 7, 8, 10, 12
|
||||||
|
giinverteddiminished ftgen 0, 0, 9, -2, 0, 1, 3, 4, 6, 7, 9, 10, 12
|
||||||
|
giionian ftgen 0, 0, 7, -2, 0, 2, 4, 5, 7, 9, 11
|
||||||
|
giionian ftgen 0, 0, 8, -2, 0, 2, 4, 5, 7, 9, 11, 12
|
||||||
|
giiwato ftgen 0, 0, 6, -2, 0, 1, 5, 6, 10, 12
|
||||||
|
gijapanese ftgen 0, 0, 6, -2, 0, 1, 5, 7, 8, 12
|
||||||
|
gijavanese ftgen 0, 0, 8, -2, 0, 1, 3, 5, 7, 9, 10, 12
|
||||||
|
gijewish ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 10, 12
|
||||||
|
gikumoi ftgen 0, 0, 6, -2, 0, 1, 5, 7, 8, 12
|
||||||
|
gileadingwholetone ftgen 0, 0, 8, -2, 0, 2, 4, 6, 8, 10, 11, 12
|
||||||
|
gilocrian ftgen 0, 0, 7, -2, 0, 1, 3, 5, 6, 8, 10
|
||||||
|
gilocrian ftgen 0, 0, 8, -2, 0, 1, 3, 5, 6, 8, 10, 12
|
||||||
|
gilocrianmajor ftgen 0, 0, 8, -2, 0, 2, 4, 5, 6, 8, 10, 12
|
||||||
|
gilocriannatural ftgen 0, 0, 8, -2, 0, 2, 3, 5, 6, 8, 10, 12
|
||||||
|
gilocriansuper ftgen 0, 0, 8, -2, 0, 1, 3, 4, 6, 8, 10, 12
|
||||||
|
gilocrianultra ftgen 0, 0, 8, -2, 0, 1, 3, 4, 6, 8, 9, 12
|
||||||
|
gilydian ftgen 0, 0, 7, -2, 0, 2, 4, 6, 7, 9, 11
|
||||||
|
gilydian ftgen 0, 0, 8, -2, 0, 2, 4, 6, 7, 9, 11, 12
|
||||||
|
gilydianaugmented ftgen 0, 0, 8, -2, 0, 2, 4, 6, 8, 9, 10, 12
|
||||||
|
gilydiandominant ftgen 0, 0, 8, -2, 0, 2, 4, 6, 7, 9, 10, 12
|
||||||
|
gilydianminor ftgen 0, 0, 8, -2, 0, 2, 4, 6, 7, 8, 10, 12
|
||||||
|
gim7 ftgen 0, 0, 19, -2, 0, 3, 7, 10, 14, 17, 21, 24, 27, 31, 34, 38, 41, 45, 48, 51, 55, 58, 62
|
||||||
|
gimajor ftgen 0, 0, 7, -2, 0, 2, 4, 5, 7, 9, 11
|
||||||
|
gimarva ftgen 0, 0, 8, -2, 0, 1, 4, 6, 7, 9, 11, 12
|
||||||
|
gimelodicminor ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 9, 11, 12
|
||||||
|
gimelodicminorascending ftgen 0, 0, 7, -2, 0, 2, 3, 5, 7, 9, 11
|
||||||
|
gimelodicminordescending ftgen 0, 0, 7, -2, 0, 2, 3, 5, 7, 8, 10
|
||||||
|
giminor ftgen 0, 0, 7, -2, 0, 2, 3, 5, 7, 8, 10
|
||||||
|
giminor3 ftgen 0, 0, 7, -2, 0, 2, 2.75, 5, 7.15, 8, 10
|
||||||
|
gimjnor ftgen 0, 0, 7, -2, 0, 1.75, 2.75, 5.35, 7.35, 8, 10
|
||||||
|
giminor2v5 ftgen 0, 0, 7, -2, 0, 2, 2.5, 5, 7, 8, 10
|
||||||
|
gimixolydian ftgen 0, 0, 7, -2, 0, 2, 4, 5, 7, 9, 10
|
||||||
|
gimixolydian ftgen 0, 0, 8, -2, 0, 2, 4, 5, 7, 9, 10, 12
|
||||||
|
gimixolydianaugmented ftgen 0, 0, 8, -2, 0, 2, 4, 5, 8, 9, 10, 12
|
||||||
|
gimohammedan ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 8, 11, 12
|
||||||
|
gimongolian ftgen 0, 0, 6, -2, 0, 2, 4, 7, 9, 12
|
||||||
|
gimonotone ftgen 0, 0, 1, -2, 0
|
||||||
|
ginaturalminor ftgen 0, 0, 7, -2, 9, 11, 0, 2, 4, 5, 7
|
||||||
|
ginaturalminor ftgen 0, 0, 8, -2, 0, 2, 3, 5, 7, 8, 10, 12
|
||||||
|
gineapolitanmajor ftgen 0, 0, 8, -2, 0, 1, 3, 5, 7, 9, 11, 12
|
||||||
|
gineapolitanminor ftgen 0, 0, 8, -2, 0, 1, 3, 5, 7, 8, 11, 12
|
||||||
|
giocta_1_2 ftgen 0, 0, 8, -2, 0, 1, 3, 4, 6, 7, 9, 10
|
||||||
|
giocta_2_1 ftgen 0, 0, 8, -2, 0, 2, 3, 5, 6, 8, 9, 11
|
||||||
|
gioriental ftgen 0, 0, 8, -2, 0, 1, 4, 5, 6, 9, 10, 12
|
||||||
|
giovertone ftgen 0, 0, 8, -2, 0, 2, 4, 6, 7, 9, 10, 12
|
||||||
|
gipa ftgen 0, 0, 6, -2, 0, 2, 3, 7, 8, 12
|
||||||
|
gipb ftgen 0, 0, 6, -2, 0, 1, 3, 6, 8, 12
|
||||||
|
gipd ftgen 0, 0, 6, -2, 0, 2, 3, 7, 9, 12
|
||||||
|
gipe ftgen 0, 0, 6, -2, 0, 1, 3, 7, 8, 12
|
||||||
|
gipelog ftgen 0, 0, 6, -2, 0, 1, 3, 7, 10, 12
|
||||||
|
gipentamaj ftgen 0, 0, 5, -2, 0, 2, 4, 7, 9
|
||||||
|
gipentamin ftgen 0, 0, 5, -2, 0, 3, 5, 7, 10
|
||||||
|
gipentatonicmajor ftgen 0, 0, 6, -2, 0, 2, 4, 7, 9, 12
|
||||||
|
gipentatonicminor ftgen 0, 0, 6, -2, 0, 3, 5, 7, 10, 12
|
||||||
|
gipersian ftgen 0, 0, 8, -2, 0, 1, 4, 5, 6, 8, 11, 12
|
||||||
|
gipfcg ftgen 0, 0, 6, -2, 0, 2, 4, 7, 9, 12
|
||||||
|
giphrygian ftgen 0, 0, 7, -2, 0, 1, 3, 5, 7, 8, 10
|
||||||
|
giphrygian ftgen 0, 0, 8, -2, 0, 1, 3, 5, 7, 8, 10, 12
|
||||||
|
giphrygianmajor ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 10, 12
|
||||||
|
giquarter ftgen 0, 0, 22, -2, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5
|
||||||
|
giromanian ftgen 0, 0, 8, -2, 0, 2, 3, 6, 7, 9, 10, 12
|
||||||
|
gispanish ftgen 0, 0, 8, -2, 0, 1, 4, 5, 7, 8, 10, 12
|
||||||
|
gispanish8tone ftgen 0, 0, 9, -2, 0, 1, 3, 4, 5, 6, 8, 10, 12
|
||||||
|
gisymmetrical ftgen 0, 0, 9, -2, 0, 1, 3, 4, 6, 7, 9, 10, 12
|
||||||
|
gitodi ftgen 0, 0, 8, -2, 0, 1, 3, 6, 7, 8, 11, 12
|
||||||
|
giwhole ftgen 0, 0, 6, -2, 0, 2, 4, 6, 8, 10
|
||||||
|
giwhole ftgen 0, 0, 7, -2, 0, 2, 4, 6, 8, 10, 12
|
||||||
|
gibp ftgen 0, 0, 12, -2, 0, 1.3324, 3.0185, 4.3508, 5.8251, 7.3693, 8.8436, 10.1760, 11.6502, 13.1944, 14.6687, 16.0011, 17.6872, 19.0196
|
||||||
|
|
||||||
|
#define chinese #gichinese#
|
||||||
|
#define persian #gipersian#
|
||||||
|
#define indian #giindian#
|
||||||
|
#define arabian #giarabian#
|
||||||
|
#define minor #giminor#
|
||||||
|
#define major #gimajor#
|
||||||
|
|
||||||
|
opcode scale_up, k, ikkk
|
||||||
|
iscale, kmin, kmax, kspeed xin
|
||||||
|
ktrig = metro:k(kspeed)
|
||||||
|
idiff = abs( i(kmax) - i(kmin) )
|
||||||
|
ibase = i(kmin)
|
||||||
|
kpick init 0
|
||||||
|
knote init ibase
|
||||||
|
kitv init 0
|
||||||
|
ilen = ftlen(iscale)
|
||||||
|
if(ktrig == 1) then
|
||||||
|
kpick = (kpick + 1) ;% ilen
|
||||||
|
if(kpick == ilen) then
|
||||||
|
kpick = 0
|
||||||
|
kitv = 0
|
||||||
|
endif
|
||||||
|
kitv = table(kpick, iscale) - kitv
|
||||||
|
printk2 kitv
|
||||||
|
knote = knote + kitv
|
||||||
|
if(knote > kmax) then
|
||||||
|
knote = kmin + (knote - kmax)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
xout knote
|
||||||
|
endop
|
||||||
|
|
||||||
|
/*
|
||||||
|
opcode scale_up, k, ikkk
|
||||||
|
iscale, kmin, kmax, kspeed xin
|
||||||
|
ktrig = metro:k(kspeed)
|
||||||
|
idiff = abs( i(kmax) - i(kmin) )
|
||||||
|
ibase = i(kmin) + (idiff/2)
|
||||||
|
kpick init 0
|
||||||
|
knote init ibase
|
||||||
|
ilen = ftlen(iscale)
|
||||||
|
if(ktrig == 1) then
|
||||||
|
kpick = (kpick + 1) % ilen
|
||||||
|
kitv = table(kpick, iscale)
|
||||||
|
knote = knote + kitv
|
||||||
|
if(knote > kmax) then
|
||||||
|
knote = kmin + (knote - kmax)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
xout knote
|
||||||
|
endop
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
opcode scale_down, k, ikkk
|
||||||
|
iscale, kmin, kmax, kspeed xin
|
||||||
|
ktrig = metro:k(kspeed)
|
||||||
|
|
||||||
|
idiff = abs( i(kmax) - i(kmin) )
|
||||||
|
ibase = i(kmax)
|
||||||
|
kpick init 0
|
||||||
|
knote init ibase
|
||||||
|
ilen = ftlen(iscale)
|
||||||
|
if(ktrig == 1) then
|
||||||
|
kpick = (kpick + 1) % ilen
|
||||||
|
kitv = table(kpick, iscale)
|
||||||
|
knote = knote - kitv
|
||||||
|
if(knote < kmin) then
|
||||||
|
knote = kmax ;- (kmin - knote)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
xout knote
|
||||||
|
endop
|
||||||
|
|
||||||
|
opcode scale_random, k, ikkk
|
||||||
|
iscale, kmin, kmax, kspeed xin
|
||||||
|
ktrig = metro:k(kspeed)
|
||||||
|
idiff = abs( i(kmax) - i(kmin) )
|
||||||
|
ibase = i(kmin) + (idiff/2)
|
||||||
|
kpick init 0
|
||||||
|
knote init ibase
|
||||||
|
ilen = ftlen(iscale)
|
||||||
|
if(ktrig == 1) then
|
||||||
|
ksign = (rint:k(0, 1) == 0) ? -1 : 1
|
||||||
|
kpick = (kpick + 1) % ilen
|
||||||
|
kitv = table(kpick, iscale)
|
||||||
|
knote = knote + (kitv * ksign)
|
||||||
|
if(knote < kmin) then
|
||||||
|
knote = kmax ;- (kmin - knote)
|
||||||
|
endif
|
||||||
|
if(knote > kmax) then
|
||||||
|
knote = kmin ;+ (knote - kmax)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
xout knote
|
||||||
|
endop
|
||||||
1039
public/system-files/synth.orc
Normal file
1039
public/system-files/synth.orc
Normal file
File diff suppressed because it is too large
Load Diff
250
src/App.svelte
250
src/App.svelte
@ -9,22 +9,15 @@
|
|||||||
import AudioScope from "./lib/components/audio/AudioScope.svelte";
|
import AudioScope from "./lib/components/audio/AudioScope.svelte";
|
||||||
import Spectrogram from "./lib/components/audio/Spectrogram.svelte";
|
import Spectrogram from "./lib/components/audio/Spectrogram.svelte";
|
||||||
import ConfirmDialog from "./lib/components/ui/ConfirmDialog.svelte";
|
import ConfirmDialog from "./lib/components/ui/ConfirmDialog.svelte";
|
||||||
import InputDialog from "./lib/components/ui/InputDialog.svelte";
|
|
||||||
import TemplateDialog from "./lib/components/ui/TemplateDialog.svelte";
|
|
||||||
import CsoundReference from "./lib/components/reference/CsoundReference.svelte";
|
import CsoundReference from "./lib/components/reference/CsoundReference.svelte";
|
||||||
import {
|
import {
|
||||||
createCsoundDerivedStores,
|
createCsoundDerivedStores,
|
||||||
type LogEntry,
|
type LogEntry,
|
||||||
type EvalSource,
|
type EvalSource,
|
||||||
} from "./lib/csound";
|
} from "./lib/csound";
|
||||||
import { type CsoundProject } from "./lib/project-system";
|
import { type File } from "./lib/project-system";
|
||||||
import {
|
import { loadOpenTabs, loadCurrentFileId } from "./lib/project-system/persistence";
|
||||||
templateRegistry,
|
|
||||||
type CsoundTemplate,
|
|
||||||
} from "./lib/templates/template-registry";
|
|
||||||
import { loadLastProjectId } from "./lib/project-system/persistence";
|
|
||||||
import { createAppContext, setAppContext } from "./lib/contexts/app-context";
|
import { createAppContext, setAppContext } from "./lib/contexts/app-context";
|
||||||
import type { ProjectMode } from "./lib/project-system/types";
|
|
||||||
import { themes, applyTheme } from "./lib/themes";
|
import { themes, applyTheme } from "./lib/themes";
|
||||||
import {
|
import {
|
||||||
Save,
|
Save,
|
||||||
@ -42,9 +35,9 @@
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
csound,
|
csound,
|
||||||
projectManager,
|
fileManager,
|
||||||
editorSettings,
|
editorSettings,
|
||||||
projectEditor,
|
editorState,
|
||||||
uiState,
|
uiState,
|
||||||
executionContext,
|
executionContext,
|
||||||
} = appContext;
|
} = appContext;
|
||||||
@ -57,44 +50,38 @@
|
|||||||
let logsUnsubscribe: (() => void) | undefined;
|
let logsUnsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await projectManager.init();
|
await fileManager.init();
|
||||||
|
await editorState.refreshFileCache();
|
||||||
|
|
||||||
const lastProjectId = loadLastProjectId();
|
// Try to restore open tabs
|
||||||
let projectToLoad: CsoundProject | null = null;
|
const openTabIds = loadOpenTabs();
|
||||||
|
const currentFileId = loadCurrentFileId();
|
||||||
|
|
||||||
if (lastProjectId) {
|
if (openTabIds.length > 0) {
|
||||||
const result = await projectManager.getProject(lastProjectId);
|
// Restore tabs
|
||||||
if (result.success) {
|
for (const fileId of openTabIds) {
|
||||||
projectToLoad = result.data;
|
await editorState.openFile(fileId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projectToLoad) {
|
// Switch to last current file
|
||||||
const allProjectsResult = await projectManager.getAllProjects();
|
if (currentFileId && openTabIds.includes(currentFileId)) {
|
||||||
if (allProjectsResult.success) {
|
await editorState.switchToFile(currentFileId);
|
||||||
if (allProjectsResult.data.length > 0) {
|
}
|
||||||
projectToLoad = allProjectsResult.data[0];
|
|
||||||
} else {
|
} else {
|
||||||
const classicTemplate = templateRegistry.getById("classic");
|
// No tabs to restore, check if we have any files
|
||||||
if (classicTemplate) {
|
const allFilesResult = await fileManager.getAllFiles();
|
||||||
const createResult = await projectManager.createProject({
|
if (allFilesResult.success && allFilesResult.data.length > 0) {
|
||||||
title: "Welcome",
|
// Open the first file
|
||||||
author: "System",
|
await editorState.openFile(allFilesResult.data[0].id);
|
||||||
content: classicTemplate.content,
|
} else {
|
||||||
tags: [],
|
// No files at all, create a welcome file
|
||||||
mode: classicTemplate.mode,
|
const welcomeContent = '<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n\nsr = 44100\nksmps = 32\nnchnls = 2\n0dbfs = 1\n\ninstr 1\n kEnv madsr 0.1, 0.2, 0.7, 0.5\n aOut oscil kEnv * 0.3, 440\n outs aOut, aOut\nendin\n\n</CsInstruments>\n<CsScore>\ni 1 0 2\n</CsScore>\n</CsoundSynthesizer>';
|
||||||
});
|
const result = await fileManager.createFile({ title: 'Welcome.orc', content: welcomeContent });
|
||||||
if (createResult.success) {
|
if (result.success) {
|
||||||
projectToLoad = createResult.data;
|
await editorState.openFile(result.data.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectToLoad) {
|
|
||||||
projectEditor.loadProject(projectToLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
logsUnsubscribe = csoundLogs.subscribe((logs) => {
|
logsUnsubscribe = csoundLogs.subscribe((logs) => {
|
||||||
interpreterLogs = logs;
|
interpreterLogs = logs;
|
||||||
@ -134,94 +121,65 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleEditorChange(value: string) {
|
function handleEditorChange(value: string) {
|
||||||
projectEditor.setContent(value);
|
editorState.setContent(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleNewEmptyFile() {
|
async function handleNewFile() {
|
||||||
const emptyTemplate = templateRegistry.getEmpty();
|
await editorState.createNewFile();
|
||||||
const result = projectEditor.requestSwitch(async () =>
|
|
||||||
await projectEditor.createNew(emptyTemplate.content, emptyTemplate.mode),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result === "confirm-unsaved") {
|
|
||||||
uiState.showUnsavedChangesDialog();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewFromTemplate() {
|
async function handleFileOpen(fileId: string) {
|
||||||
uiState.showTemplateDialog();
|
await editorState.openFile(fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTemplateSelect(template: CsoundTemplate) {
|
async function handleFileDelete(fileId: string) {
|
||||||
uiState.hideTemplateDialog();
|
await editorState.deleteFile(fileId);
|
||||||
|
|
||||||
const result = projectEditor.requestSwitch(async () =>
|
|
||||||
await projectEditor.createNew(template.content, template.mode),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result === "confirm-unsaved") {
|
|
||||||
uiState.showUnsavedChangesDialog();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(project: CsoundProject | null) {
|
async function handleFileRename(fileId: string, newTitle: string) {
|
||||||
if (!project) return;
|
await editorState.renameFile(fileId, newTitle);
|
||||||
|
|
||||||
const result = projectEditor.requestSwitch(() =>
|
|
||||||
projectEditor.loadProject(project),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result === "confirm-unsaved") {
|
|
||||||
uiState.showUnsavedChangesDialog();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExecute(code: string, source: EvalSource) {
|
async function handleTabClick(fileId: string) {
|
||||||
|
await editorState.switchToFile(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTabClose(fileId: string) {
|
||||||
|
await editorState.closeFile(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExecuteFile() {
|
||||||
try {
|
try {
|
||||||
await executionContext.execute(code, source);
|
await executionContext.executeFile();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Execution error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExecuteBlock(code: string) {
|
||||||
|
try {
|
||||||
|
await executionContext.executeBlock(code);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Execution error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExecuteSelection(code: string) {
|
||||||
|
try {
|
||||||
|
await executionContext.executeSelection(code);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Execution error:", error);
|
console.error("Execution error:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
await projectEditor.save();
|
await editorState.save();
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveAs(title: string) {
|
|
||||||
const success = await projectEditor.saveAs(title);
|
|
||||||
if (success) {
|
|
||||||
uiState.hideSaveAsDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMetadataUpdate(
|
|
||||||
projectId: string,
|
|
||||||
updates: {
|
|
||||||
title?: string;
|
|
||||||
author?: string;
|
|
||||||
mode?: import("./lib/project-system/types").ProjectMode;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
await projectEditor.updateMetadata(updates);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSwitchSave() {
|
|
||||||
await projectEditor.confirmSaveAndSwitch();
|
|
||||||
uiState.hideUnsavedChangesDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSwitchDiscard() {
|
|
||||||
projectEditor.confirmDiscardAndSwitch();
|
|
||||||
uiState.hideUnsavedChangesDialog();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleShare() {
|
async function handleShare() {
|
||||||
if (!projectEditor.currentProjectId) return;
|
if (!editorState.currentFileId) return;
|
||||||
|
|
||||||
const result = await projectManager.exportProjectToUrl(
|
const result = await fileManager.exportFilesToUrl([editorState.currentFileId]);
|
||||||
projectEditor.currentProjectId,
|
|
||||||
);
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(result.data);
|
await navigator.clipboard.writeText(result.data);
|
||||||
@ -262,18 +220,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const mode = projectEditor.currentProject?.mode || "composition";
|
executionContext.setContentProvider(() => editorState.content);
|
||||||
const projectId = projectEditor.currentProjectId;
|
|
||||||
|
|
||||||
if (mode !== executionContext.mode) {
|
|
||||||
executionContext.switchMode(mode).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
executionContext.setContentProvider(() => projectEditor.content);
|
|
||||||
|
|
||||||
if (projectId) {
|
|
||||||
executionContext.startSession(projectId).catch(console.error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const panelTabs = [
|
const panelTabs = [
|
||||||
@ -301,12 +248,11 @@
|
|||||||
|
|
||||||
{#snippet filesTabContent()}
|
{#snippet filesTabContent()}
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
{projectManager}
|
{fileManager}
|
||||||
onFileSelect={handleFileSelect}
|
onFileOpen={handleFileOpen}
|
||||||
onNewEmptyFile={handleNewEmptyFile}
|
onFileDelete={handleFileDelete}
|
||||||
onNewFromTemplate={handleNewFromTemplate}
|
onFileRename={handleFileRename}
|
||||||
onMetadataUpdate={handleMetadataUpdate}
|
onNewFile={handleNewFile}
|
||||||
selectedProjectId={projectEditor.currentProjectId}
|
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
@ -327,27 +273,27 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={handleNewFromTemplate}
|
onclick={handleNewFile}
|
||||||
title="New from template"
|
title="New file"
|
||||||
>
|
>
|
||||||
<FileStack size={18} />
|
<FileStack size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={handleSave}
|
onclick={handleSave}
|
||||||
disabled={!projectEditor.hasUnsavedChanges}
|
disabled={!editorState.hasUnsavedChanges}
|
||||||
title="Save (Ctrl+S){projectEditor.hasUnsavedChanges
|
title="Save (Ctrl+S){editorState.hasUnsavedChanges
|
||||||
? ' - unsaved changes'
|
? ' - unsaved changes'
|
||||||
: ''}"
|
: ''}"
|
||||||
class:has-changes={projectEditor.hasUnsavedChanges}
|
class:has-changes={editorState.hasUnsavedChanges}
|
||||||
>
|
>
|
||||||
<Save size={18} />
|
<Save size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={handleShare}
|
onclick={handleShare}
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
disabled={!projectEditor.currentProjectId}
|
disabled={!editorState.currentFileId}
|
||||||
title="Share project"
|
title="Share current file"
|
||||||
>
|
>
|
||||||
<Share2 size={18} />
|
<Share2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -357,7 +303,7 @@
|
|||||||
class="icon-button evaluate-button"
|
class="icon-button evaluate-button"
|
||||||
disabled={!$initialized}
|
disabled={!$initialized}
|
||||||
class:is-running={$running}
|
class:is-running={$running}
|
||||||
title="Evaluate (Cmd-E)"
|
title="Evaluate Block (Cmd-E)"
|
||||||
>
|
>
|
||||||
<Play size={18} />
|
<Play size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -417,12 +363,19 @@
|
|||||||
<div class="editor-area">
|
<div class="editor-area">
|
||||||
<EditorWithLogs
|
<EditorWithLogs
|
||||||
bind:this={editorWithLogsRef}
|
bind:this={editorWithLogsRef}
|
||||||
value={projectEditor.content}
|
value={editorState.content}
|
||||||
|
fileName={editorState.currentFile?.title}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
onExecute={handleExecute}
|
onExecuteFile={handleExecuteFile}
|
||||||
|
onExecuteBlock={handleExecuteBlock}
|
||||||
|
onExecuteSelection={handleExecuteSelection}
|
||||||
logs={interpreterLogs}
|
logs={interpreterLogs}
|
||||||
{editorSettings}
|
{editorSettings}
|
||||||
mode={projectEditor.currentProject?.mode || 'composition'}
|
openFiles={editorState.openFiles}
|
||||||
|
currentFileId={editorState.currentFileId}
|
||||||
|
unsavedFileIds={new Set(Array.from(editorState.unsavedChanges.keys()))}
|
||||||
|
onTabClick={handleTabClick}
|
||||||
|
onTabClose={handleTabClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -520,29 +473,6 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</ResizablePopup>
|
</ResizablePopup>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:visible={uiState.unsavedChangesDialogVisible}
|
|
||||||
title="Unsaved Changes"
|
|
||||||
message="You have unsaved changes. What would you like to do?"
|
|
||||||
confirmLabel="Save"
|
|
||||||
cancelLabel="Discard"
|
|
||||||
onConfirm={handleSwitchSave}
|
|
||||||
onCancel={handleSwitchDiscard}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputDialog
|
|
||||||
bind:visible={uiState.saveAsDialogVisible}
|
|
||||||
title="Save As"
|
|
||||||
label="File name"
|
|
||||||
placeholder="Untitled"
|
|
||||||
onConfirm={handleSaveAs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TemplateDialog
|
|
||||||
bind:visible={uiState.templateDialogVisible}
|
|
||||||
onSelect={handleTemplateSelect}
|
|
||||||
onCancel={() => uiState.hideTemplateDialog()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -23,20 +23,34 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
|
fileName?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
onExecuteFile?: () => void;
|
||||||
|
onExecuteBlock?: (code: string) => void;
|
||||||
|
onExecuteSelection?: (code: string) => void;
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
mode: 'composition' | 'livecoding';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value = '',
|
value = '',
|
||||||
|
fileName,
|
||||||
onChange,
|
onChange,
|
||||||
onExecute,
|
onExecuteFile,
|
||||||
editorSettings,
|
onExecuteBlock,
|
||||||
mode = 'composition'
|
onExecuteSelection,
|
||||||
|
editorSettings
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
function getFileTypeFromExtension(fileName?: string): 'orc' | 'csd' | 'sco' {
|
||||||
|
if (!fileName) return 'orc';
|
||||||
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
||||||
|
if (ext === 'csd') return 'csd';
|
||||||
|
if (ext === 'sco') return 'sco';
|
||||||
|
return 'orc';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileType = $derived(getFileTypeFromExtension(fileName));
|
||||||
|
|
||||||
let editorContainer: HTMLDivElement;
|
let editorContainer: HTMLDivElement;
|
||||||
let editorView: EditorView | null = null;
|
let editorView: EditorView | null = null;
|
||||||
|
|
||||||
@ -47,40 +61,60 @@
|
|||||||
const hoverTooltipCompartment = new Compartment();
|
const hoverTooltipCompartment = new Compartment();
|
||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
|
|
||||||
export function handleExecute() {
|
export function handleExecuteFile() {
|
||||||
if (!editorView) return;
|
if (!editorView) return;
|
||||||
|
|
||||||
if (mode === 'composition') {
|
|
||||||
const doc = getDocument(editorView.state);
|
const doc = getDocument(editorView.state);
|
||||||
flash(editorView, doc.from, doc.to);
|
flash(editorView, doc.from, doc.to);
|
||||||
onExecute?.(doc.text, 'document');
|
onExecuteFile?.();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = getSelection(editorView.state);
|
|
||||||
if (selection.text) {
|
|
||||||
flash(editorView, selection.from, selection.to);
|
|
||||||
onExecute?.(selection.text, 'selection');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleExecuteBlock() {
|
||||||
|
if (!editorView) return;
|
||||||
const block = getBlock(editorView.state);
|
const block = getBlock(editorView.state);
|
||||||
if (block.text) {
|
if (block.text) {
|
||||||
flash(editorView, block.from, block.to);
|
flash(editorView, block.from, block.to);
|
||||||
onExecute?.(block.text, 'block');
|
onExecuteBlock?.(block.text);
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = getDocument(editorView.state);
|
export function handleExecuteSelection() {
|
||||||
flash(editorView, doc.from, doc.to);
|
if (!editorView) return;
|
||||||
onExecute?.(doc.text, 'document');
|
const selection = getSelection(editorView.state);
|
||||||
|
if (selection.text) {
|
||||||
|
flash(editorView, selection.from, selection.to);
|
||||||
|
onExecuteSelection?.(selection.text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluateKeymap = keymap.of([
|
export function handleExecute() {
|
||||||
|
handleExecuteBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluateBlockKeymap = keymap.of([
|
||||||
{
|
{
|
||||||
key: 'Mod-e',
|
key: 'Mod-e',
|
||||||
run: () => {
|
run: () => {
|
||||||
handleExecute();
|
handleExecuteBlock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const evaluateSelectionKeymap = keymap.of([
|
||||||
|
{
|
||||||
|
key: 'Alt-e',
|
||||||
|
run: () => {
|
||||||
|
handleExecuteSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const evaluateFileKeymap = keymap.of([
|
||||||
|
{
|
||||||
|
key: 'Mod-Shift-e',
|
||||||
|
run: () => {
|
||||||
|
handleExecuteFile();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,15 +149,15 @@
|
|||||||
|
|
||||||
const initSettings = $editorSettings;
|
const initSettings = $editorSettings;
|
||||||
|
|
||||||
const fileType = mode === 'livecoding' ? 'orc' : 'csd';
|
|
||||||
|
|
||||||
editorView = new EditorView({
|
editorView = new EditorView({
|
||||||
doc: value,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
...baseExtensions,
|
...baseExtensions,
|
||||||
languageCompartment.of(csoundMode({ fileType })),
|
languageCompartment.of(csoundMode({ fileType })),
|
||||||
themeCompartment.of(createCodeMirrorTheme()),
|
themeCompartment.of(createCodeMirrorTheme()),
|
||||||
evaluateKeymap,
|
evaluateBlockKeymap,
|
||||||
|
evaluateSelectionKeymap,
|
||||||
|
evaluateFileKeymap,
|
||||||
flashField(),
|
flashField(),
|
||||||
lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
|
lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
|
||||||
lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
|
lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
|
||||||
@ -179,7 +213,6 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!editorView) return;
|
if (!editorView) return;
|
||||||
|
|
||||||
const fileType = mode === 'livecoding' ? 'orc' : 'csd';
|
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
effects: languageCompartment.reconfigure(csoundMode({ fileType }))
|
effects: languageCompartment.reconfigure(csoundMode({ fileType }))
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,24 +2,41 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Editor from './Editor.svelte';
|
import Editor from './Editor.svelte';
|
||||||
import LogPanel from './LogPanel.svelte';
|
import LogPanel from './LogPanel.svelte';
|
||||||
|
import Tabs from '../ui/Tabs.svelte';
|
||||||
import type { EditorSettingsStore } from '../../stores/editorSettings';
|
import type { EditorSettingsStore } from '../../stores/editorSettings';
|
||||||
|
import type { File } from '../../project-system';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
|
fileName?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onExecute?: (code: string, source: 'selection' | 'block' | 'document') => void;
|
onExecuteFile?: () => void;
|
||||||
|
onExecuteBlock?: (code: string) => void;
|
||||||
|
onExecuteSelection?: (code: string) => void;
|
||||||
logs?: string[];
|
logs?: string[];
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
mode: 'composition' | 'livecoding';
|
// Tab props
|
||||||
|
openFiles?: File[];
|
||||||
|
currentFileId?: string | null;
|
||||||
|
unsavedFileIds?: Set<string>;
|
||||||
|
onTabClick?: (fileId: string) => void;
|
||||||
|
onTabClose?: (fileId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value = '',
|
value = '',
|
||||||
|
fileName,
|
||||||
onChange,
|
onChange,
|
||||||
onExecute,
|
onExecuteFile,
|
||||||
|
onExecuteBlock,
|
||||||
|
onExecuteSelection,
|
||||||
logs = [],
|
logs = [],
|
||||||
editorSettings,
|
editorSettings,
|
||||||
mode = 'composition'
|
openFiles = [],
|
||||||
|
currentFileId = null,
|
||||||
|
unsavedFileIds = new Set(),
|
||||||
|
onTabClick,
|
||||||
|
onTabClose
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let editorRef: Editor;
|
let editorRef: Editor;
|
||||||
@ -85,14 +102,24 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-with-logs">
|
<div class="editor-with-logs">
|
||||||
|
<Tabs
|
||||||
|
files={openFiles}
|
||||||
|
{currentFileId}
|
||||||
|
{unsavedFileIds}
|
||||||
|
{onTabClick}
|
||||||
|
{onTabClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="editor-section" style="height: {editorHeight}%;">
|
<div class="editor-section" style="height: {editorHeight}%;">
|
||||||
<Editor
|
<Editor
|
||||||
bind:this={editorRef}
|
bind:this={editorRef}
|
||||||
{value}
|
{value}
|
||||||
|
{fileName}
|
||||||
{onChange}
|
{onChange}
|
||||||
{onExecute}
|
{onExecuteFile}
|
||||||
|
{onExecuteBlock}
|
||||||
|
{onExecuteSelection}
|
||||||
{editorSettings}
|
{editorSettings}
|
||||||
{mode}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,331 +1,280 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { File, FilePlus, FileStack, Trash2 } from 'lucide-svelte';
|
import { File as FileIcon, FilePlus, Trash2, Check, X, Lock } from 'lucide-svelte';
|
||||||
import type { CsoundProject, ProjectManager } from '../../project-system';
|
import type { File, FileManager } from '../../project-system';
|
||||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||||
|
|
||||||
import type { ProjectMode } from '../../project-system/types';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectManager: ProjectManager;
|
fileManager: FileManager;
|
||||||
onFileSelect?: (project: CsoundProject | null) => void;
|
onFileOpen?: (fileId: string) => void;
|
||||||
onNewEmptyFile?: () => void;
|
onFileDelete?: (fileId: string) => void;
|
||||||
onNewFromTemplate?: () => void;
|
onFileRename?: (fileId: string, newTitle: string) => void;
|
||||||
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string; mode?: ProjectMode }) => void;
|
onNewFile?: () => void;
|
||||||
selectedProjectId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { projectManager, onFileSelect, onNewEmptyFile, onNewFromTemplate, onMetadataUpdate, selectedProjectId = null }: Props = $props();
|
let { fileManager, onFileOpen, onFileDelete, onFileRename, onNewFile }: Props = $props();
|
||||||
|
|
||||||
let projects = $state<CsoundProject[]>([]);
|
let files = $state<File[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let showDeleteConfirm = $state(false);
|
let showDeleteConfirm = $state(false);
|
||||||
let projectToDelete = $state<CsoundProject | null>(null);
|
let fileToDelete = $state<File | null>(null);
|
||||||
|
let editingFileId = $state<string | null>(null);
|
||||||
let selectedProject = $derived(
|
let editingTitle = $state('');
|
||||||
projects.find(p => p.id === selectedProjectId) || null
|
|
||||||
);
|
|
||||||
|
|
||||||
let editTitle = $state('');
|
|
||||||
let editAuthor = $state('');
|
|
||||||
let editMode = $state<ProjectMode>('composition');
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (selectedProject) {
|
|
||||||
editTitle = selectedProject.title;
|
|
||||||
editAuthor = selectedProject.author;
|
|
||||||
editMode = selectedProject.mode;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function refresh() {
|
|
||||||
await loadProjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadProjects();
|
await loadFiles();
|
||||||
|
|
||||||
const unsubscribe = projectManager.onProjectsChanged(() => {
|
const unsubscribe = fileManager.onFilesChanged(() => {
|
||||||
loadProjects();
|
loadFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadProjects() {
|
async function loadFiles() {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const result = await projectManager.getAllProjects();
|
const result = await fileManager.getAllFiles();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
projects = result.data.sort((a, b) =>
|
files = result.data;
|
||||||
new Date(b.dateModified).getTime() - new Date(a.dateModified).getTime()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load projects:', error);
|
console.error('Failed to load files:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewEmptyFile() {
|
function handleNewFile() {
|
||||||
onNewEmptyFile?.();
|
onNewFile?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewFromTemplate() {
|
function openFile(file: File) {
|
||||||
onNewFromTemplate?.();
|
if (editingFileId) return;
|
||||||
|
onFileOpen?.(file.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectProject(project: CsoundProject) {
|
function startDelete(file: File) {
|
||||||
onFileSelect?.(project);
|
fileToDelete = file;
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteClick(project: CsoundProject, event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
projectToDelete = project;
|
|
||||||
showDeleteConfirm = true;
|
showDeleteConfirm = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete() {
|
async function confirmDelete() {
|
||||||
if (!projectToDelete) return;
|
if (fileToDelete) {
|
||||||
|
onFileDelete?.(fileToDelete.id);
|
||||||
const result = await projectManager.deleteProject(projectToDelete.id);
|
|
||||||
if (result.success) {
|
|
||||||
await loadProjects();
|
|
||||||
}
|
}
|
||||||
projectToDelete = null;
|
showDeleteConfirm = false;
|
||||||
|
fileToDelete = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateString: string): string {
|
function cancelDelete() {
|
||||||
const date = new Date(dateString);
|
showDeleteConfirm = false;
|
||||||
return date.toLocaleDateString('en-US', {
|
fileToDelete = null;
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMetadataChange() {
|
function startEdit(file: File) {
|
||||||
if (!selectedProject) return;
|
if (file.system) return;
|
||||||
|
editingFileId = file.id;
|
||||||
|
editingTitle = file.title;
|
||||||
|
}
|
||||||
|
|
||||||
const hasChanges =
|
function saveEdit() {
|
||||||
editTitle !== selectedProject.title ||
|
if (editingFileId && editingTitle.trim()) {
|
||||||
editAuthor !== selectedProject.author ||
|
onFileRename?.(editingFileId, editingTitle.trim());
|
||||||
editMode !== selectedProject.mode;
|
}
|
||||||
|
editingFileId = null;
|
||||||
|
editingTitle = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (hasChanges) {
|
function cancelEdit() {
|
||||||
onMetadataUpdate?.(selectedProject.id, {
|
editingFileId = null;
|
||||||
title: editTitle,
|
editingTitle = '';
|
||||||
author: editAuthor,
|
}
|
||||||
mode: editMode
|
|
||||||
});
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
saveEdit();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
cancelEdit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="file-browser">
|
<div class="file-browser">
|
||||||
<div class="browser-header">
|
<div class="file-browser-header">
|
||||||
<span class="browser-title">Files</span>
|
<h3>Files</h3>
|
||||||
<div class="header-actions">
|
<button
|
||||||
<button class="action-button" onclick={handleNewEmptyFile} title="New empty file">
|
class="new-file-button"
|
||||||
<FilePlus size={18} />
|
onclick={handleNewFile}
|
||||||
|
title="New file"
|
||||||
|
>
|
||||||
|
<FilePlus size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button" onclick={handleNewFromTemplate} title="New from template">
|
|
||||||
<FileStack size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="browser-content">
|
<div class="file-list">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="empty-state">Loading...</div>
|
<div class="loading-state">Loading...</div>
|
||||||
|
{:else if files.length === 0}
|
||||||
|
<div class="empty-state">No files</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="project-list">
|
{#each files as file}
|
||||||
{#each projects as project}
|
|
||||||
<div
|
<div
|
||||||
class="project-item"
|
class="file-item"
|
||||||
class:selected={selectedProjectId === project.id}
|
class:editing={editingFileId === file.id}
|
||||||
onclick={() => selectProject(project)}
|
class:system={file.system}
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<div class="project-icon">
|
{#if editingFileId === file.id}
|
||||||
<File size={16} />
|
<div class="file-edit">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={editingTitle}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
class="file-title-input"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<button class="icon-btn save" onclick={saveEdit} title="Save">
|
||||||
|
<Check size={14} />
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn cancel" onclick={cancelEdit} title="Cancel">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
{:else}
|
||||||
<div class="project-title">{project.title}</div>
|
<div
|
||||||
|
class="file-info"
|
||||||
|
onclick={() => openFile(file)}
|
||||||
|
ondblclick={() => startEdit(file)}
|
||||||
|
>
|
||||||
|
<FileIcon size={14} />
|
||||||
|
<span class="file-title">{file.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{#if file.system}
|
||||||
|
<div class="lock-icon" title="System file (read-only)">
|
||||||
|
<Lock size={14} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="delete-button"
|
class="delete-button"
|
||||||
onclick={(e) => handleDeleteClick(project, e)}
|
onclick={() => startDelete(file)}
|
||||||
title="Delete"
|
title="Delete file"
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
{#if selectedProject}
|
|
||||||
<div class="metadata-editor">
|
|
||||||
<div class="metadata-header">Metadata</div>
|
|
||||||
<div class="metadata-fields">
|
|
||||||
<div class="field">
|
|
||||||
<label for="file-title">Name</label>
|
|
||||||
<input
|
|
||||||
id="file-title"
|
|
||||||
type="text"
|
|
||||||
bind:value={editTitle}
|
|
||||||
onchange={handleMetadataChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="file-author">Author</label>
|
|
||||||
<input
|
|
||||||
id="file-author"
|
|
||||||
type="text"
|
|
||||||
bind:value={editAuthor}
|
|
||||||
onchange={handleMetadataChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="file-mode">Mode</label>
|
|
||||||
<select
|
|
||||||
id="file-mode"
|
|
||||||
bind:value={editMode}
|
|
||||||
onchange={handleMetadataChange}
|
|
||||||
>
|
|
||||||
<option value="composition">Composition</option>
|
|
||||||
<option value="livecoding">Live Coding</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field readonly">
|
|
||||||
<label>Number of Saves</label>
|
|
||||||
<div class="readonly-value">{selectedProject.saveCount}</div>
|
|
||||||
</div>
|
|
||||||
<div class="field readonly">
|
|
||||||
<label>Date Created</label>
|
|
||||||
<div class="readonly-value">{formatDate(selectedProject.dateCreated)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="field readonly">
|
|
||||||
<label>Date Modified</label>
|
|
||||||
<div class="readonly-value">{formatDate(selectedProject.dateModified)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="field readonly">
|
|
||||||
<label>Csound Version</label>
|
|
||||||
<div class="readonly-value">{selectedProject.csoundVersion}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:visible={showDeleteConfirm}
|
bind:visible={showDeleteConfirm}
|
||||||
title="Delete File"
|
title="Delete File"
|
||||||
message="Are you sure you want to delete '{projectToDelete?.title}'?"
|
message="Are you sure you want to delete '{fileToDelete?.title}'? This cannot be undone."
|
||||||
confirmLabel="Delete"
|
confirmLabel="Delete"
|
||||||
|
cancelLabel="Cancel"
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
|
onCancel={cancelDelete}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.file-browser {
|
.file-browser {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--surface-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-header {
|
.file-browser-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
background-color: var(--surface-color);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-title {
|
h3 {
|
||||||
font-size: var(--font-sm);
|
margin: 0;
|
||||||
font-weight: 600;
|
font-size: var(--font-lg);
|
||||||
color: var(--text-secondary);
|
color: var(--text-color);
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.new-file-button {
|
||||||
display: flex;
|
padding: var(--space-sm);
|
||||||
gap: var(--space-xs);
|
background-color: var(--accent-color);
|
||||||
}
|
color: white;
|
||||||
|
|
||||||
.action-button {
|
|
||||||
padding: 0.375rem;
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all var(--transition-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button:hover {
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--editor-active-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
padding: var(--space-2xl) var(--space-lg);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-md);
|
|
||||||
padding: var(--space-md);
|
|
||||||
border-bottom: 1px solid var(--surface-color);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color var(--transition-base);
|
transition: background-color var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-item:hover {
|
.new-file-button:hover {
|
||||||
background-color: var(--surface-color);
|
background-color: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-item.selected {
|
.file-list {
|
||||||
background-color: rgba(100, 108, 255, 0.2);
|
flex: 1;
|
||||||
border-left: 3px solid var(--accent-color);
|
overflow-y: auto;
|
||||||
|
padding: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-icon {
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
text-align: center;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-info {
|
.file-item:hover:not(.editing) {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.editing {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.system {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.system:hover:not(.editing) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-title {
|
.file-title {
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
font-size: var(--font-base);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -339,77 +288,67 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all var(--transition-base);
|
transition: opacity var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-item:hover .delete-button {
|
.file-item:hover .delete-button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-button:hover {
|
.delete-button:hover {
|
||||||
color: rgba(255, 100, 100, 0.9);
|
color: var(--danger-color);
|
||||||
background-color: rgba(255, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-editor {
|
.lock-icon {
|
||||||
border-top: 1px solid var(--surface-color);
|
padding: var(--space-xs);
|
||||||
background-color: var(--bg-color);
|
|
||||||
padding: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-header {
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
display: flex;
|
||||||
letter-spacing: 0.05em;
|
align-items: center;
|
||||||
margin-bottom: var(--space-lg);
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-fields {
|
.file-edit {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field label {
|
.file-title-input {
|
||||||
font-size: var(--font-sm);
|
flex: 1;
|
||||||
color: var(--text-secondary);
|
padding: var(--space-xs) var(--space-sm);
|
||||||
}
|
background-color: var(--bg-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
.field input,
|
|
||||||
.field select {
|
|
||||||
padding: var(--space-sm);
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: var(--font-base);
|
font-size: var(--font-base);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field input:focus,
|
.icon-btn {
|
||||||
.field select:focus {
|
padding: var(--space-xs);
|
||||||
border-color: var(--accent-color);
|
border: none;
|
||||||
}
|
|
||||||
|
|
||||||
.field select {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field.readonly .readonly-value {
|
.icon-btn.save {
|
||||||
padding: var(--space-sm);
|
background-color: var(--accent-color);
|
||||||
background-color: var(--bg-color);
|
color: white;
|
||||||
border: 1px solid var(--surface-color);
|
}
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-base);
|
.icon-btn.save:hover {
|
||||||
font-family: monospace;
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.cancel {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.cancel:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
121
src/lib/components/ui/Tabs.svelte
Normal file
121
src/lib/components/ui/Tabs.svelte
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { X } from 'lucide-svelte';
|
||||||
|
import type { File } from '../../project-system';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
files: File[];
|
||||||
|
currentFileId: string | null;
|
||||||
|
unsavedFileIds: Set<string>;
|
||||||
|
onTabClick?: (fileId: string) => void;
|
||||||
|
onTabClose?: (fileId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { files, currentFileId, unsavedFileIds, onTabClick, onTabClose }: Props = $props();
|
||||||
|
|
||||||
|
function handleTabClick(fileId: string) {
|
||||||
|
onTabClick?.(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose(e: MouseEvent, fileId: string) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTabClose?.(fileId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="tabs-container">
|
||||||
|
{#if files.length === 0}
|
||||||
|
<div class="no-tabs">No files open</div>
|
||||||
|
{:else}
|
||||||
|
{#each files as file}
|
||||||
|
<div
|
||||||
|
class="tab"
|
||||||
|
class:active={currentFileId === file.id}
|
||||||
|
onclick={() => handleTabClick(file.id)}
|
||||||
|
>
|
||||||
|
<span class="tab-title">{file.title}</span>
|
||||||
|
{#if unsavedFileIds.has(file.id)}
|
||||||
|
<span class="unsaved-indicator">●</span>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="tab-close"
|
||||||
|
onclick={(e) => handleClose(e, file.id)}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tabs {
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsaved-indicator {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
padding: 2px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover .tab-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,129 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { templateRegistry, type CsoundTemplate } from '../../templates/template-registry';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
visible: boolean;
|
|
||||||
onSelect?: (template: CsoundTemplate) => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { visible = false, onSelect, onCancel }: Props = $props();
|
|
||||||
|
|
||||||
const templates = templateRegistry.getAll();
|
|
||||||
|
|
||||||
function handleSelect(template: CsoundTemplate) {
|
|
||||||
onSelect?.(template);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBackdropClick(e: MouseEvent) {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
onCancel?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if visible}
|
|
||||||
<div class="modal-backdrop" onclick={handleBackdropClick}>
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Choose Template</h2>
|
|
||||||
<button class="close-button" onclick={() => onCancel?.()}>×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="template-list">
|
|
||||||
{#each templates as template}
|
|
||||||
<button
|
|
||||||
class="template-item"
|
|
||||||
onclick={() => handleSelect(template)}
|
|
||||||
>
|
|
||||||
{template.name}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
width: 400px;
|
|
||||||
max-height: 80vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 2rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: color var(--transition-base);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button:hover {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-item {
|
|
||||||
padding: var(--space-lg);
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--surface-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color var(--transition-base);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-item:hover {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,37 +1,37 @@
|
|||||||
import { getContext, setContext } from 'svelte';
|
import { getContext, setContext } from 'svelte';
|
||||||
import { ProjectManager } from '../project-system/project-manager';
|
import { FileManager } from '../project-system/file-manager';
|
||||||
import { ProjectDatabase } from '../project-system/db';
|
import { FileDatabase } from '../project-system/db';
|
||||||
import { createCsoundStore } from '../csound/store';
|
import { createCsoundStore } from '../csound/store';
|
||||||
import { ExecutionContext } from '../csound/execution-context';
|
import { ExecutionContext } from '../csound/execution-context';
|
||||||
import { createEditorSettingsStore } from '../stores/editorSettings';
|
import { createEditorSettingsStore } from '../stores/editorSettings';
|
||||||
import { ProjectEditor } from '../stores/projectEditor.svelte';
|
import { EditorState } from '../stores/editorState.svelte';
|
||||||
import { UIState } from '../stores/uiState.svelte';
|
import { UIState } from '../stores/uiState.svelte';
|
||||||
import type { CsoundStore } from '../csound/store';
|
import type { CsoundStore } from '../csound/store';
|
||||||
import type { EditorSettingsStore } from '../stores/editorSettings';
|
import type { EditorSettingsStore } from '../stores/editorSettings';
|
||||||
|
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
projectManager: ProjectManager;
|
fileManager: FileManager;
|
||||||
csound: CsoundStore;
|
csound: CsoundStore;
|
||||||
executionContext: ExecutionContext;
|
executionContext: ExecutionContext;
|
||||||
editorSettings: EditorSettingsStore;
|
editorSettings: EditorSettingsStore;
|
||||||
projectEditor: ProjectEditor;
|
editorState: EditorState;
|
||||||
uiState: UIState;
|
uiState: UIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const APP_CONTEXT_KEY = Symbol('app-context');
|
const APP_CONTEXT_KEY = Symbol('app-context');
|
||||||
|
|
||||||
export function createAppContext(): AppContext {
|
export function createAppContext(): AppContext {
|
||||||
const db = new ProjectDatabase();
|
const db = new FileDatabase();
|
||||||
const projectManager = new ProjectManager(db);
|
const fileManager = new FileManager(db);
|
||||||
const csound = createCsoundStore();
|
const csound = createCsoundStore();
|
||||||
const executionContext = new ExecutionContext(csound, 'composition');
|
const executionContext = new ExecutionContext(csound);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectManager,
|
fileManager,
|
||||||
csound,
|
csound,
|
||||||
executionContext,
|
executionContext,
|
||||||
editorSettings: createEditorSettingsStore(),
|
editorSettings: createEditorSettingsStore(),
|
||||||
projectEditor: new ProjectEditor(projectManager),
|
editorState: new EditorState(fileManager),
|
||||||
uiState: new UIState()
|
uiState: new UIState()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,102 +1,48 @@
|
|||||||
import type { CsoundStore } from './store';
|
import type { CsoundStore } from './store';
|
||||||
import type { ProjectMode } from '../project-system/types';
|
|
||||||
import { CompositionStrategy, LiveCodingStrategy, type ExecutionStrategy } from './execution-strategies';
|
|
||||||
|
|
||||||
export type EvalSource = 'selection' | 'block' | 'document';
|
export type EvalSource = 'selection' | 'block' | 'file';
|
||||||
|
|
||||||
export interface ExecutionSession {
|
|
||||||
mode: ProjectMode;
|
|
||||||
projectId: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
startTime: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExecutionContext {
|
export class ExecutionContext {
|
||||||
private session: ExecutionSession | null = null;
|
|
||||||
private strategy: ExecutionStrategy;
|
|
||||||
private currentMode: ProjectMode;
|
|
||||||
private contentProvider: (() => string) | null = null;
|
private contentProvider: (() => string) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(private csound: CsoundStore) {}
|
||||||
private csound: CsoundStore,
|
|
||||||
mode: ProjectMode
|
|
||||||
) {
|
|
||||||
this.currentMode = mode;
|
|
||||||
this.strategy = this.createStrategyForMode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
get mode(): ProjectMode {
|
|
||||||
return this.currentMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeSession(): ExecutionSession | null {
|
|
||||||
return this.session;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentProvider(provider: () => string): void {
|
setContentProvider(provider: () => string): void {
|
||||||
this.contentProvider = provider;
|
this.contentProvider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startSession(projectId: string | null): Promise<void> {
|
/**
|
||||||
await this.endSession();
|
* Execute entire file
|
||||||
|
*/
|
||||||
this.session = {
|
async executeFile(): Promise<void> {
|
||||||
mode: this.currentMode,
|
const content = this.contentProvider?.() ?? '';
|
||||||
projectId,
|
if (!content.trim()) {
|
||||||
isActive: true,
|
return;
|
||||||
startTime: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.currentMode === 'livecoding') {
|
|
||||||
this.resetStrategy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async endSession(): Promise<void> {
|
|
||||||
if (!this.session?.isActive) return;
|
|
||||||
|
|
||||||
if (this.currentMode === 'livecoding') {
|
|
||||||
await this.csound.stop();
|
await this.csound.stop();
|
||||||
this.resetStrategy();
|
await this.csound.evaluateCode(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session = { ...this.session, isActive: false };
|
/**
|
||||||
|
* Execute code block (instrument, opcode, or paragraph)
|
||||||
|
*/
|
||||||
|
async executeBlock(code: string): Promise<void> {
|
||||||
|
if (!code.trim()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(code: string, source: EvalSource): Promise<void> {
|
await this.csound.evaluateCode(code);
|
||||||
if (this.currentMode === 'livecoding' && (!this.session || !this.session.isActive)) {
|
|
||||||
await this.startSession(this.session?.projectId ?? null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullContent = this.contentProvider?.() ?? '';
|
/**
|
||||||
await this.strategy.execute(this.csound, code, fullContent, source);
|
* Execute selected text
|
||||||
|
*/
|
||||||
|
async executeSelection(code: string): Promise<void> {
|
||||||
|
if (!code.trim()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchMode(newMode: ProjectMode): Promise<void> {
|
await this.csound.evaluateCode(code);
|
||||||
if (newMode === this.currentMode) return;
|
|
||||||
|
|
||||||
await this.endSession();
|
|
||||||
this.currentMode = newMode;
|
|
||||||
this.strategy = this.createStrategyForMode(newMode);
|
|
||||||
|
|
||||||
if (newMode === 'composition') {
|
|
||||||
await this.csound.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createStrategyForMode(mode: ProjectMode): ExecutionStrategy {
|
|
||||||
return mode === 'livecoding'
|
|
||||||
? new LiveCodingStrategy()
|
|
||||||
: new CompositionStrategy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetStrategy(): void {
|
|
||||||
if (this.strategy instanceof LiveCodingStrategy) {
|
|
||||||
this.strategy.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
|
||||||
await this.endSession();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import pako from 'pako';
|
import pako from 'pako';
|
||||||
import type { CsoundProject, CompressedProject } from './types';
|
import type { File, CompressedFiles } from './types';
|
||||||
|
|
||||||
const COMPRESSION_VERSION = 1;
|
const COMPRESSION_VERSION = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a string to a Uint8Array
|
* Convert a string to a Uint8Array
|
||||||
@ -59,12 +59,12 @@ function base64ToUint8Array(base64: string): Uint8Array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compress a project to a base64 string for sharing
|
* Compress files to a base64 string for sharing
|
||||||
*/
|
*/
|
||||||
export function compressProject(project: CsoundProject): CompressedProject {
|
export function compressFiles(files: File[]): CompressedFiles {
|
||||||
try {
|
try {
|
||||||
// Convert project to JSON string
|
// Convert files to JSON string
|
||||||
const jsonString = JSON.stringify(project);
|
const jsonString = JSON.stringify(files);
|
||||||
|
|
||||||
// Convert to Uint8Array
|
// Convert to Uint8Array
|
||||||
const uint8Array = stringToUint8Array(jsonString);
|
const uint8Array = stringToUint8Array(jsonString);
|
||||||
@ -80,14 +80,14 @@ export function compressProject(project: CsoundProject): CompressedProject {
|
|||||||
version: COMPRESSION_VERSION,
|
version: COMPRESSION_VERSION,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to compress project: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
throw new Error(`Failed to compress files: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decompress a base64 string back to a project
|
* Decompress a base64 string back to files
|
||||||
*/
|
*/
|
||||||
export function decompressProject(compressed: CompressedProject): CsoundProject {
|
export function decompressFiles(compressed: CompressedFiles): File[] {
|
||||||
try {
|
try {
|
||||||
// Check version compatibility
|
// Check version compatibility
|
||||||
if (compressed.version !== COMPRESSION_VERSION) {
|
if (compressed.version !== COMPRESSION_VERSION) {
|
||||||
@ -104,24 +104,24 @@ export function decompressProject(compressed: CompressedProject): CsoundProject
|
|||||||
const jsonString = uint8ArrayToString(decompressed);
|
const jsonString = uint8ArrayToString(decompressed);
|
||||||
|
|
||||||
// Parse JSON
|
// Parse JSON
|
||||||
const project = JSON.parse(jsonString) as CsoundProject;
|
const files = JSON.parse(jsonString) as File[];
|
||||||
|
|
||||||
// Validate that we have the required fields
|
// Validate that we have an array
|
||||||
if (!project.id || !project.title || !project.content === undefined) {
|
if (!Array.isArray(files)) {
|
||||||
throw new Error('Invalid project data structure');
|
throw new Error('Invalid files data structure');
|
||||||
}
|
}
|
||||||
|
|
||||||
return project;
|
return files;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to decompress project: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
throw new Error(`Failed to decompress files: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a shareable URL from a project
|
* Create a shareable URL from files
|
||||||
*/
|
*/
|
||||||
export function projectToShareUrl(project: CsoundProject, baseUrl: string = window.location.origin): string {
|
export function filesToShareUrl(files: File[], baseUrl: string = window.location.origin): string {
|
||||||
const compressed = compressProject(project);
|
const compressed = compressFiles(files);
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
v: compressed.version.toString(),
|
v: compressed.version.toString(),
|
||||||
d: compressed.data,
|
d: compressed.data,
|
||||||
@ -131,33 +131,33 @@ export function projectToShareUrl(project: CsoundProject, baseUrl: string = wind
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract a project from a URL
|
* Extract files from a URL
|
||||||
*/
|
*/
|
||||||
export function projectFromShareUrl(url: string): CsoundProject {
|
export function filesFromShareUrl(url: string): File[] {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const params = urlObj.searchParams;
|
const params = urlObj.searchParams;
|
||||||
|
|
||||||
const version = parseInt(params.get('v') || '1', 10);
|
const version = parseInt(params.get('v') || '3', 10);
|
||||||
const data = params.get('d');
|
const data = params.get('d');
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error('No project data found in URL');
|
throw new Error('No files data found in URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
const compressed: CompressedProject = { version, data };
|
const compressed: CompressedFiles = { version, data };
|
||||||
return decompressProject(compressed);
|
return decompressFiles(compressed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to parse project from URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
throw new Error(`Failed to parse files from URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the approximate compression ratio
|
* Calculate the approximate compression ratio
|
||||||
*/
|
*/
|
||||||
export function getCompressionRatio(project: CsoundProject): number {
|
export function getCompressionRatio(files: File[]): number {
|
||||||
const original = JSON.stringify(project);
|
const original = JSON.stringify(files);
|
||||||
const compressed = compressProject(project);
|
const compressed = compressFiles(files);
|
||||||
|
|
||||||
const originalSize = new TextEncoder().encode(original).length;
|
const originalSize = new TextEncoder().encode(original).length;
|
||||||
const compressedSize = base64ToUint8Array(compressed.data).length;
|
const compressedSize = base64ToUint8Array(compressed.data).length;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import type { CsoundProject } from './types';
|
import type { File } from './types';
|
||||||
|
|
||||||
const DB_NAME = 'csound-projects-db';
|
const DB_NAME = 'csound-files-db';
|
||||||
const DB_VERSION = 1;
|
const DB_VERSION = 3;
|
||||||
const STORE_NAME = 'projects';
|
const STORE_NAME = 'files';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database wrapper for IndexedDB operations
|
* Database wrapper for IndexedDB operations
|
||||||
*/
|
*/
|
||||||
class ProjectDatabase {
|
class FileDatabase {
|
||||||
private db: IDBDatabase | null = null;
|
private db: IDBDatabase | null = null;
|
||||||
private initPromise: Promise<void> | null = null;
|
private initPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
@ -38,18 +38,19 @@ class ProjectDatabase {
|
|||||||
request.onupgradeneeded = (event) => {
|
request.onupgradeneeded = (event) => {
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
|
||||||
// Create object store if it doesn't exist
|
// Delete old stores if they exist
|
||||||
|
if (db.objectStoreNames.contains('projects')) {
|
||||||
|
db.deleteObjectStore('projects');
|
||||||
|
}
|
||||||
|
if (db.objectStoreNames.contains('workspaces')) {
|
||||||
|
db.deleteObjectStore('workspaces');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create files store if it doesn't exist
|
||||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
const objectStore = db.createObjectStore(STORE_NAME, {
|
db.createObjectStore(STORE_NAME, {
|
||||||
keyPath: 'id',
|
keyPath: 'id',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create indexes for efficient querying
|
|
||||||
objectStore.createIndex('title', 'title', { unique: false });
|
|
||||||
objectStore.createIndex('author', 'author', { unique: false });
|
|
||||||
objectStore.createIndex('dateCreated', 'dateCreated', { unique: false });
|
|
||||||
objectStore.createIndex('dateModified', 'dateModified', { unique: false });
|
|
||||||
objectStore.createIndex('tags', 'tags', { unique: false, multiEntry: true });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -69,9 +70,9 @@ class ProjectDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a project by ID
|
* Get a file by ID
|
||||||
*/
|
*/
|
||||||
async get(id: string): Promise<CsoundProject | null> {
|
async get(id: string): Promise<File | null> {
|
||||||
const db = await this.ensureDb();
|
const db = await this.ensureDb();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -84,15 +85,15 @@ class ProjectDatabase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error(`Failed to get project: ${request.error?.message}`));
|
reject(new Error(`Failed to get file: ${request.error?.message}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all projects
|
* Get all files
|
||||||
*/
|
*/
|
||||||
async getAll(): Promise<CsoundProject[]> {
|
async getAll(): Promise<File[]> {
|
||||||
const db = await this.ensureDb();
|
const db = await this.ensureDb();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -105,34 +106,34 @@ class ProjectDatabase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error(`Failed to get all projects: ${request.error?.message}`));
|
reject(new Error(`Failed to get all files: ${request.error?.message}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save or update a project
|
* Save or update a file
|
||||||
*/
|
*/
|
||||||
async put(project: CsoundProject): Promise<void> {
|
async put(file: File): Promise<void> {
|
||||||
const db = await this.ensureDb();
|
const db = await this.ensureDb();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
const store = transaction.objectStore(STORE_NAME);
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
const request = store.put(project);
|
const request = store.put(file);
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error(`Failed to save project: ${request.error?.message}`));
|
reject(new Error(`Failed to save file: ${request.error?.message}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a project by ID
|
* Delete a file by ID
|
||||||
*/
|
*/
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
const db = await this.ensureDb();
|
const db = await this.ensureDb();
|
||||||
@ -147,57 +148,13 @@ class ProjectDatabase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error(`Failed to delete project: ${request.error?.message}`));
|
reject(new Error(`Failed to delete file: ${request.error?.message}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search projects by tag
|
* Clear all files (use with caution!)
|
||||||
*/
|
|
||||||
async getByTag(tag: string): Promise<CsoundProject[]> {
|
|
||||||
const db = await this.ensureDb();
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
|
||||||
const store = transaction.objectStore(STORE_NAME);
|
|
||||||
const index = store.index('tags');
|
|
||||||
const request = index.getAll(tag);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
resolve(request.result || []);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
reject(new Error(`Failed to get projects by tag: ${request.error?.message}`));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search projects by author
|
|
||||||
*/
|
|
||||||
async getByAuthor(author: string): Promise<CsoundProject[]> {
|
|
||||||
const db = await this.ensureDb();
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
|
||||||
const store = transaction.objectStore(STORE_NAME);
|
|
||||||
const index = store.index('author');
|
|
||||||
const request = index.getAll(author);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
resolve(request.result || []);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = () => {
|
|
||||||
reject(new Error(`Failed to get projects by author: ${request.error?.message}`));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all projects (use with caution!)
|
|
||||||
*/
|
*/
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
const db = await this.ensureDb();
|
const db = await this.ensureDb();
|
||||||
@ -212,7 +169,7 @@ class ProjectDatabase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error(`Failed to clear projects: ${request.error?.message}`));
|
reject(new Error(`Failed to clear files: ${request.error?.message}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -229,4 +186,4 @@ class ProjectDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ProjectDatabase };
|
export { FileDatabase };
|
||||||
|
|||||||
316
src/lib/project-system/file-manager.ts
Normal file
316
src/lib/project-system/file-manager.ts
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import type { File, CreateFileData, Result } from './types';
|
||||||
|
import type { FileDatabase } from './db';
|
||||||
|
import { compressFiles, decompressFiles, filesToShareUrl, filesFromShareUrl } from './compression';
|
||||||
|
import { loadSystemFiles, isSystemFileId, getSystemFile } from './system-files';
|
||||||
|
|
||||||
|
async function wrapResult<T>(fn: () => Promise<T>, errorMsg: string): Promise<Result<T>> {
|
||||||
|
try {
|
||||||
|
const data = await fn();
|
||||||
|
return { success: true, data };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error(errorMsg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique ID
|
||||||
|
*/
|
||||||
|
function generateId(): string {
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileChangeListener = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File Manager - Main API for managing files
|
||||||
|
*/
|
||||||
|
export class FileManager {
|
||||||
|
private db: FileDatabase;
|
||||||
|
private changeListeners: Set<FileChangeListener> = new Set();
|
||||||
|
|
||||||
|
constructor(db: FileDatabase) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to file changes
|
||||||
|
*/
|
||||||
|
onFilesChanged(listener: FileChangeListener): () => void {
|
||||||
|
this.changeListeners.add(listener);
|
||||||
|
return () => this.changeListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify all listeners that files have changed
|
||||||
|
*/
|
||||||
|
private notifyChange(): void {
|
||||||
|
this.changeListeners.forEach(listener => listener());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the file manager (initializes database)
|
||||||
|
*/
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await this.db.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new file
|
||||||
|
*/
|
||||||
|
async createFile(data: CreateFileData): Promise<Result<File>> {
|
||||||
|
try {
|
||||||
|
const file: File = {
|
||||||
|
id: generateId(),
|
||||||
|
title: data.title,
|
||||||
|
content: data.content || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.db.put(file);
|
||||||
|
this.notifyChange();
|
||||||
|
|
||||||
|
return { success: true, data: file };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to create file'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a file by ID (checks both user and system files)
|
||||||
|
*/
|
||||||
|
async getFile(id: string): Promise<Result<File>> {
|
||||||
|
try {
|
||||||
|
if (isSystemFileId(id)) {
|
||||||
|
const systemFile = getSystemFile(id);
|
||||||
|
if (systemFile) {
|
||||||
|
return { success: true, data: systemFile };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await this.db.get(id);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: new Error(`File not found: ${id}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: file };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to get file'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all files (user files + system files)
|
||||||
|
*/
|
||||||
|
async getAllFiles(): Promise<Result<File[]>> {
|
||||||
|
try {
|
||||||
|
const userFiles = await this.db.getAll();
|
||||||
|
const systemFiles = await loadSystemFiles();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: [...systemFiles, ...userFiles],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to get files'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a file (system files cannot be updated directly)
|
||||||
|
*/
|
||||||
|
async updateFile(id: string, updates: Partial<Pick<File, 'title' | 'content'>>): Promise<Result<File>> {
|
||||||
|
try {
|
||||||
|
if (isSystemFileId(id)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: new Error('Cannot update system files directly. Use "Save As" to create a copy.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingFile = await this.db.get(id);
|
||||||
|
|
||||||
|
if (!existingFile) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: new Error(`File not found: ${id}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFile: File = {
|
||||||
|
...existingFile,
|
||||||
|
...(updates.title !== undefined && { title: updates.title }),
|
||||||
|
...(updates.content !== undefined && { content: updates.content }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.db.put(updatedFile);
|
||||||
|
this.notifyChange();
|
||||||
|
|
||||||
|
return { success: true, data: updatedFile };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to update file'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file (system files cannot be deleted)
|
||||||
|
*/
|
||||||
|
async deleteFile(id: string): Promise<Result<void>> {
|
||||||
|
if (isSystemFileId(id)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: new Error('Cannot delete system files'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await wrapResult(() => this.db.delete(id), 'Failed to delete file');
|
||||||
|
if (result.success) {
|
||||||
|
this.notifyChange();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate a file (works with both user and system files)
|
||||||
|
*/
|
||||||
|
async duplicateFile(id: string): Promise<Result<File>> {
|
||||||
|
try {
|
||||||
|
const result = await this.getFile(id);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalFile = result.data;
|
||||||
|
const duplicatedFile: File = {
|
||||||
|
id: generateId(),
|
||||||
|
title: `${originalFile.title} (copy)`,
|
||||||
|
content: originalFile.content,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.db.put(duplicatedFile);
|
||||||
|
this.notifyChange();
|
||||||
|
|
||||||
|
return { success: true, data: duplicatedFile };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to duplicate file'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate unique untitled filename
|
||||||
|
*/
|
||||||
|
async generateUntitledFilename(): Promise<string> {
|
||||||
|
const result = await this.getAllFiles();
|
||||||
|
if (!result.success) {
|
||||||
|
return 'Untitled-1.orc';
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = result.data;
|
||||||
|
const untitledPattern = /^Untitled-(\d+)\.orc$/;
|
||||||
|
let maxNum = 0;
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const match = file.title.match(untitledPattern);
|
||||||
|
if (match) {
|
||||||
|
const num = parseInt(match[1], 10);
|
||||||
|
if (num > maxNum) {
|
||||||
|
maxNum = num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return `Untitled-${maxNum + 1}.orc`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export files to a shareable URL
|
||||||
|
*/
|
||||||
|
async exportFilesToUrl(fileIds: string[], baseUrl?: string): Promise<Result<string>> {
|
||||||
|
try {
|
||||||
|
const files: File[] = [];
|
||||||
|
|
||||||
|
for (const id of fileIds) {
|
||||||
|
const file = await this.db.get(id);
|
||||||
|
if (file) {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: new Error('No files to export'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = filesToShareUrl(files, baseUrl);
|
||||||
|
|
||||||
|
return { success: true, data: url };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to export files'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import files from a URL
|
||||||
|
*/
|
||||||
|
async importFilesFromUrl(url: string): Promise<Result<File[]>> {
|
||||||
|
try {
|
||||||
|
const files = filesFromShareUrl(url);
|
||||||
|
|
||||||
|
// Generate new IDs for imported files
|
||||||
|
const importedFiles = files.map(file => ({
|
||||||
|
...file,
|
||||||
|
id: generateId(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const file of importedFiles) {
|
||||||
|
await this.db.put(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyChange();
|
||||||
|
|
||||||
|
return { success: true, data: importedFiles };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error('Failed to import files'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all files (use with caution!)
|
||||||
|
*/
|
||||||
|
async clearAllFiles(): Promise<Result<void>> {
|
||||||
|
const result = await wrapResult(() => this.db.clear(), 'Failed to clear files');
|
||||||
|
if (result.success) {
|
||||||
|
this.notifyChange();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,56 +1,40 @@
|
|||||||
/**
|
/**
|
||||||
* Csound Project Management System
|
* Csound File Management System
|
||||||
*
|
*
|
||||||
* This module provides a complete project system for managing Csound code files
|
* This module provides a file system for managing Csound files
|
||||||
* with browser-based storage (IndexedDB) and import/export functionality via
|
* with browser-based storage (IndexedDB) and import/export functionality via
|
||||||
* compressed URLs.
|
* compressed URLs.
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import { projectManager } from './lib/project-system';
|
|
||||||
*
|
|
||||||
* // Initialize
|
|
||||||
* await projectManager.init();
|
|
||||||
*
|
|
||||||
* // Create a new project
|
|
||||||
* const result = await projectManager.createProject({
|
|
||||||
* title: 'My First Csound Project',
|
|
||||||
* author: 'John Doe',
|
|
||||||
* content: '<CsoundSynthesizer>...</CsoundSynthesizer>',
|
|
||||||
* tags: ['synth', 'experiment']
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Get all projects
|
|
||||||
* const projects = await projectManager.getAllProjects();
|
|
||||||
*
|
|
||||||
* // Export to shareable URL
|
|
||||||
* const urlResult = await projectManager.exportProjectToUrl(result.data.id);
|
|
||||||
*
|
|
||||||
* // Import from URL
|
|
||||||
* const imported = await projectManager.importProjectFromUrl(url);
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type {
|
export type {
|
||||||
CsoundProject,
|
File,
|
||||||
CreateProjectData,
|
CreateFileData,
|
||||||
UpdateProjectData,
|
CompressedFiles,
|
||||||
CompressedProject,
|
|
||||||
Result,
|
Result,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// Export main API
|
// Export main API
|
||||||
export { ProjectManager } from './project-manager';
|
export { FileManager } from './file-manager';
|
||||||
|
|
||||||
// Export database (for advanced usage)
|
// Export database
|
||||||
export { projectDb } from './db';
|
export { FileDatabase } from './db';
|
||||||
|
|
||||||
// Export compression utilities (for advanced usage)
|
// Export compression utilities
|
||||||
export {
|
export {
|
||||||
compressProject,
|
compressFiles,
|
||||||
decompressProject,
|
decompressFiles,
|
||||||
projectToShareUrl,
|
filesToShareUrl,
|
||||||
projectFromShareUrl,
|
filesFromShareUrl,
|
||||||
getCompressionRatio,
|
getCompressionRatio,
|
||||||
} from './compression';
|
} from './compression';
|
||||||
|
|
||||||
|
// Export persistence utilities
|
||||||
|
export {
|
||||||
|
saveOpenTabs,
|
||||||
|
loadOpenTabs,
|
||||||
|
clearOpenTabs,
|
||||||
|
saveCurrentFileId,
|
||||||
|
loadCurrentFileId,
|
||||||
|
clearCurrentFileId,
|
||||||
|
} from './persistence';
|
||||||
|
|||||||
@ -1,17 +1,39 @@
|
|||||||
const LAST_PROJECT_KEY = 'oldboy:lastProjectId';
|
const OPEN_TABS_KEY = 'oldboy:openTabs';
|
||||||
|
const CURRENT_FILE_KEY = 'oldboy:currentFileId';
|
||||||
|
|
||||||
export function saveLastProjectId(projectId: string | null): void {
|
export function saveOpenTabs(fileIds: string[]): void {
|
||||||
if (projectId === null) {
|
localStorage.setItem(OPEN_TABS_KEY, JSON.stringify(fileIds));
|
||||||
localStorage.removeItem(LAST_PROJECT_KEY);
|
}
|
||||||
|
|
||||||
|
export function loadOpenTabs(): string[] {
|
||||||
|
const stored = localStorage.getItem(OPEN_TABS_KEY);
|
||||||
|
if (!stored) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearOpenTabs(): void {
|
||||||
|
localStorage.removeItem(OPEN_TABS_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveCurrentFileId(fileId: string | null): void {
|
||||||
|
if (fileId === null) {
|
||||||
|
localStorage.removeItem(CURRENT_FILE_KEY);
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(LAST_PROJECT_KEY, projectId);
|
localStorage.setItem(CURRENT_FILE_KEY, fileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadLastProjectId(): string | null {
|
export function loadCurrentFileId(): string | null {
|
||||||
return localStorage.getItem(LAST_PROJECT_KEY);
|
return localStorage.getItem(CURRENT_FILE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearLastProjectId(): void {
|
export function clearCurrentFileId(): void {
|
||||||
localStorage.removeItem(LAST_PROJECT_KEY);
|
localStorage.removeItem(CURRENT_FILE_KEY);
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/lib/project-system/system-files.ts
Normal file
68
src/lib/project-system/system-files.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { File } from './types';
|
||||||
|
|
||||||
|
const SYSTEM_FILES_DIR = '/system-files';
|
||||||
|
|
||||||
|
const SYSTEM_FILE_NAMES = [
|
||||||
|
'archives.orc',
|
||||||
|
'Documentation.orc',
|
||||||
|
'examples.orc',
|
||||||
|
'globals.orc',
|
||||||
|
'lib.orc',
|
||||||
|
'livecode.orc',
|
||||||
|
'scale.orc',
|
||||||
|
'synth.orc',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
let systemFilesCache: File[] | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all system files from the public directory
|
||||||
|
*/
|
||||||
|
export async function loadSystemFiles(): Promise<File[]> {
|
||||||
|
if (systemFilesCache) {
|
||||||
|
return systemFilesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: File[] = [];
|
||||||
|
|
||||||
|
for (const fileName of SYSTEM_FILE_NAMES) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${SYSTEM_FILES_DIR}/${fileName}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to load system file: ${fileName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await response.text();
|
||||||
|
files.push({
|
||||||
|
id: `system:${fileName}`,
|
||||||
|
title: fileName,
|
||||||
|
content,
|
||||||
|
readonly: true,
|
||||||
|
system: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading system file ${fileName}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemFilesCache = files;
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file ID corresponds to a system file
|
||||||
|
*/
|
||||||
|
export function isSystemFileId(id: string): boolean {
|
||||||
|
return id.startsWith('system:');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a system file by ID
|
||||||
|
*/
|
||||||
|
export function getSystemFile(id: string): File | undefined {
|
||||||
|
if (!systemFilesCache) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return systemFilesCache.find((file) => file.id === id);
|
||||||
|
}
|
||||||
@ -1,68 +1,36 @@
|
|||||||
export type ProjectMode = 'composition' | 'livecoding';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core data structure for a Csound project
|
* A single file
|
||||||
*/
|
*/
|
||||||
export interface CsoundProject {
|
export interface File {
|
||||||
/** Unique identifier for the project */
|
/** Unique identifier for the file */
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
/** User-defined project title */
|
/** Filename (e.g., "kick.orc", "main.csd") */
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
/** Project author name */
|
|
||||||
author: string;
|
|
||||||
|
|
||||||
/** Date when the project was created (ISO string) */
|
|
||||||
dateCreated: string;
|
|
||||||
|
|
||||||
/** Date when the project was last modified (ISO string) */
|
|
||||||
dateModified: string;
|
|
||||||
|
|
||||||
/** Number of times the project has been saved */
|
|
||||||
saveCount: number;
|
|
||||||
|
|
||||||
/** The Csound code content */
|
/** The Csound code content */
|
||||||
content: string;
|
content: string;
|
||||||
|
|
||||||
/** Optional tags for categorization */
|
/** Whether this file is read-only (can be viewed/edited but requires "Save As") */
|
||||||
tags: string[];
|
readonly?: boolean;
|
||||||
|
|
||||||
/** Csound version used to create this project */
|
/** Whether this is a system file (cannot be deleted, always visible) */
|
||||||
csoundVersion: string;
|
system?: boolean;
|
||||||
|
|
||||||
/** Execution mode: composition (full document) or livecoding (block evaluation) */
|
|
||||||
mode: ProjectMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data structure for creating a new project (omits auto-generated fields)
|
* Data structure for creating a new file
|
||||||
*/
|
*/
|
||||||
export interface CreateProjectData {
|
export interface CreateFileData {
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
|
||||||
content?: string;
|
content?: string;
|
||||||
tags?: string[];
|
|
||||||
mode?: ProjectMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data structure for updating an existing project
|
* Compressed files data for import/export via links
|
||||||
*/
|
*/
|
||||||
export interface UpdateProjectData {
|
export interface CompressedFiles {
|
||||||
id: string;
|
/** Base64-encoded compressed files data */
|
||||||
title?: string;
|
|
||||||
author?: string;
|
|
||||||
content?: string;
|
|
||||||
tags?: string[];
|
|
||||||
mode?: ProjectMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compressed project data for import/export via links
|
|
||||||
*/
|
|
||||||
export interface CompressedProject {
|
|
||||||
/** Base64-encoded compressed project data */
|
|
||||||
data: string;
|
data: string;
|
||||||
|
|
||||||
/** Version of the compression format (for future compatibility) */
|
/** Version of the compression format (for future compatibility) */
|
||||||
|
|||||||
248
src/lib/stores/editorState.svelte.ts
Normal file
248
src/lib/stores/editorState.svelte.ts
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import type { File, FileManager } from '../project-system';
|
||||||
|
import { saveOpenTabs, saveCurrentFileId } from '../project-system/persistence';
|
||||||
|
|
||||||
|
interface EditorStateData {
|
||||||
|
openFileIds: string[];
|
||||||
|
currentFileId: string | null;
|
||||||
|
unsavedChanges: Map<string, string>; // fileId -> modified content
|
||||||
|
files: Map<string, File>; // fileId -> file data
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditorState {
|
||||||
|
private fileManager: FileManager;
|
||||||
|
|
||||||
|
private state = $state<EditorStateData>({
|
||||||
|
openFileIds: [],
|
||||||
|
currentFileId: null,
|
||||||
|
unsavedChanges: new Map(),
|
||||||
|
files: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
private pendingAction: (() => void) | null = null;
|
||||||
|
|
||||||
|
constructor(fileManager: FileManager) {
|
||||||
|
this.fileManager = fileManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get openFileIds() {
|
||||||
|
return this.state.openFileIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentFileId() {
|
||||||
|
return this.state.currentFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentFile() {
|
||||||
|
if (!this.state.currentFileId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.state.files.get(this.state.currentFileId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get content() {
|
||||||
|
if (!this.state.currentFileId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// Return unsaved content if exists, otherwise file content
|
||||||
|
return this.state.unsavedChanges.get(this.state.currentFileId) ?? this.currentFile?.content ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasUnsavedChanges() {
|
||||||
|
if (!this.state.currentFileId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.state.unsavedChanges.has(this.state.currentFileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCurrentFileReadonly() {
|
||||||
|
return this.currentFile?.readonly ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get openFiles() {
|
||||||
|
return this.state.openFileIds
|
||||||
|
.map(id => this.state.files.get(id))
|
||||||
|
.filter((f): f is File => f !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
get unsavedChanges() {
|
||||||
|
return this.state.unsavedChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUnsavedChangesForFile(fileId: string): boolean {
|
||||||
|
return this.state.unsavedChanges.has(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent(content: string) {
|
||||||
|
if (!this.state.currentFileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.unsavedChanges.set(this.state.currentFileId, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openFile(fileId: string) {
|
||||||
|
// Load file if not in cache
|
||||||
|
if (!this.state.files.has(fileId)) {
|
||||||
|
const result = await this.fileManager.getFile(fileId);
|
||||||
|
if (!result.success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.state.files.set(fileId, result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to open tabs if not already open
|
||||||
|
if (!this.state.openFileIds.includes(fileId)) {
|
||||||
|
this.state.openFileIds.push(fileId);
|
||||||
|
saveOpenTabs(this.state.openFileIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to this file
|
||||||
|
this.state.currentFileId = fileId;
|
||||||
|
saveCurrentFileId(fileId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeFile(fileId: string) {
|
||||||
|
// Check for unsaved changes
|
||||||
|
if (this.state.unsavedChanges.has(fileId)) {
|
||||||
|
// TODO: prompt user?
|
||||||
|
// For now, just discard changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from open tabs
|
||||||
|
this.state.openFileIds = this.state.openFileIds.filter(id => id !== fileId);
|
||||||
|
saveOpenTabs(this.state.openFileIds);
|
||||||
|
|
||||||
|
// Clear unsaved changes
|
||||||
|
this.state.unsavedChanges.delete(fileId);
|
||||||
|
|
||||||
|
// If this was the current file, switch to another
|
||||||
|
if (this.state.currentFileId === fileId) {
|
||||||
|
if (this.state.openFileIds.length > 0) {
|
||||||
|
this.state.currentFileId = this.state.openFileIds[0];
|
||||||
|
saveCurrentFileId(this.state.currentFileId);
|
||||||
|
} else {
|
||||||
|
this.state.currentFileId = null;
|
||||||
|
saveCurrentFileId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchToFile(fileId: string) {
|
||||||
|
if (!this.state.openFileIds.includes(fileId)) {
|
||||||
|
return await this.openFile(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.currentFileId = fileId;
|
||||||
|
saveCurrentFileId(fileId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!this.state.currentFileId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = this.state.unsavedChanges.get(this.state.currentFileId);
|
||||||
|
if (content === undefined) {
|
||||||
|
return true; // Nothing to save
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.fileManager.updateFile(this.state.currentFileId, { content });
|
||||||
|
if (result.success) {
|
||||||
|
// Update file cache
|
||||||
|
this.state.files.set(this.state.currentFileId, result.data);
|
||||||
|
// Clear unsaved changes
|
||||||
|
this.state.unsavedChanges.delete(this.state.currentFileId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFile(fileId: string) {
|
||||||
|
const content = this.state.unsavedChanges.get(fileId);
|
||||||
|
if (content === undefined) {
|
||||||
|
return true; // Nothing to save
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.fileManager.updateFile(fileId, { content });
|
||||||
|
if (result.success) {
|
||||||
|
this.state.files.set(fileId, result.data);
|
||||||
|
this.state.unsavedChanges.delete(fileId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewFile() {
|
||||||
|
const title = await this.fileManager.generateUntitledFilename();
|
||||||
|
const result = await this.fileManager.createFile({ title, content: '' });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.state.files.set(result.data.id, result.data);
|
||||||
|
await this.openFile(result.data.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renameFile(fileId: string, newTitle: string) {
|
||||||
|
const result = await this.fileManager.updateFile(fileId, { title: newTitle });
|
||||||
|
if (result.success) {
|
||||||
|
this.state.files.set(fileId, result.data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(fileId: string) {
|
||||||
|
const result = await this.fileManager.deleteFile(fileId);
|
||||||
|
if (result.success) {
|
||||||
|
this.state.files.delete(fileId);
|
||||||
|
await this.closeFile(fileId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshFileCache() {
|
||||||
|
const result = await this.fileManager.getAllFiles();
|
||||||
|
if (result.success) {
|
||||||
|
this.state.files.clear();
|
||||||
|
result.data.forEach(file => {
|
||||||
|
this.state.files.set(file.id, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSwitch(action: () => void): 'proceed' | 'confirm-unsaved' {
|
||||||
|
if (!this.hasUnsavedChanges) {
|
||||||
|
action();
|
||||||
|
return 'proceed';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingAction = action;
|
||||||
|
return 'confirm-unsaved';
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmSaveAndSwitch(): Promise<void> {
|
||||||
|
await this.save();
|
||||||
|
this.pendingAction?.();
|
||||||
|
this.pendingAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDiscardAndSwitch(): void {
|
||||||
|
if (this.state.currentFileId) {
|
||||||
|
this.state.unsavedChanges.delete(this.state.currentFileId);
|
||||||
|
}
|
||||||
|
this.pendingAction?.();
|
||||||
|
this.pendingAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelSwitch(): void {
|
||||||
|
this.pendingAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,8 +9,6 @@ export class UIState {
|
|||||||
sharePopupVisible = $state(false);
|
sharePopupVisible = $state(false);
|
||||||
audioPermissionPopupVisible = $state(true);
|
audioPermissionPopupVisible = $state(true);
|
||||||
unsavedChangesDialogVisible = $state(false);
|
unsavedChangesDialogVisible = $state(false);
|
||||||
saveAsDialogVisible = $state(false);
|
|
||||||
templateDialogVisible = $state(false);
|
|
||||||
|
|
||||||
shareUrl = $state('');
|
shareUrl = $state('');
|
||||||
|
|
||||||
@ -58,20 +56,4 @@ export class UIState {
|
|||||||
hideUnsavedChangesDialog() {
|
hideUnsavedChangesDialog() {
|
||||||
this.unsavedChangesDialogVisible = false;
|
this.unsavedChangesDialogVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
showSaveAsDialog() {
|
|
||||||
this.saveAsDialogVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideSaveAsDialog() {
|
|
||||||
this.saveAsDialogVisible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
showTemplateDialog() {
|
|
||||||
this.templateDialogVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideTemplateDialog() {
|
|
||||||
this.templateDialogVisible = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,162 +0,0 @@
|
|||||||
import type { ProjectMode } from '../project-system/types';
|
|
||||||
|
|
||||||
export interface CsoundTemplate {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
mode: ProjectMode;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_TEMPLATE: CsoundTemplate = {
|
|
||||||
id: 'empty',
|
|
||||||
name: 'Empty',
|
|
||||||
mode: 'composition',
|
|
||||||
content: `<CsoundSynthesizer>
|
|
||||||
<CsOptions>
|
|
||||||
-odac
|
|
||||||
</CsOptions>
|
|
||||||
<CsInstruments>
|
|
||||||
|
|
||||||
sr = 48000
|
|
||||||
ksmps = 32
|
|
||||||
nchnls = 2
|
|
||||||
0dbfs = 1
|
|
||||||
|
|
||||||
</CsInstruments>
|
|
||||||
<CsScore>
|
|
||||||
|
|
||||||
</CsScore>
|
|
||||||
</CsoundSynthesizer>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
const CLASSIC_TEMPLATE: CsoundTemplate = {
|
|
||||||
id: 'classic',
|
|
||||||
name: 'Classic',
|
|
||||||
mode: 'composition',
|
|
||||||
content: `<CsoundSynthesizer>
|
|
||||||
<CsOptions>
|
|
||||||
-odac
|
|
||||||
</CsOptions>
|
|
||||||
<CsInstruments>
|
|
||||||
|
|
||||||
sr = 48000
|
|
||||||
ksmps = 32
|
|
||||||
nchnls = 2
|
|
||||||
0dbfs = 1
|
|
||||||
|
|
||||||
instr 1
|
|
||||||
iFreq = p4
|
|
||||||
iAmp = p5
|
|
||||||
|
|
||||||
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
|
||||||
|
|
||||||
aOsc oscili iAmp * kEnv, iFreq
|
|
||||||
|
|
||||||
outs aOsc, aOsc
|
|
||||||
endin
|
|
||||||
|
|
||||||
</CsInstruments>
|
|
||||||
<CsScore>
|
|
||||||
i 1 0.0 0.5 261.63 0.3
|
|
||||||
i 1 0.5 0.5 329.63 0.3
|
|
||||||
i 1 1.0 0.5 392.00 0.3
|
|
||||||
i 1 1.5 0.5 523.25 0.3
|
|
||||||
</CsScore>
|
|
||||||
</CsoundSynthesizer>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
const LIVECODING_TEMPLATE: CsoundTemplate = {
|
|
||||||
id: 'livecoding',
|
|
||||||
name: 'Live Coding',
|
|
||||||
mode: 'livecoding',
|
|
||||||
content: `gaReverb init 0
|
|
||||||
|
|
||||||
instr 1
|
|
||||||
kFreq chnget "freq"
|
|
||||||
kFreq = (kFreq == 0 ? p4 : kFreq)
|
|
||||||
kAmp = p5
|
|
||||||
kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0
|
|
||||||
aOsc vco2 kAmp * kEnv, kFreq
|
|
||||||
aFilt moogladder aOsc, 2000, 0.3
|
|
||||||
outs aFilt, aFilt
|
|
||||||
gaReverb = gaReverb + aFilt * 0.3
|
|
||||||
endin
|
|
||||||
|
|
||||||
instr 2
|
|
||||||
iFreq = p4
|
|
||||||
iAmp = p5
|
|
||||||
kEnv linsegr 0, 0.005, 1, 0.05, 0.5, 0.1, 0
|
|
||||||
aOsc vco2 iAmp * kEnv, iFreq, 10
|
|
||||||
aFilt butterlp aOsc, 800
|
|
||||||
outs aFilt, aFilt
|
|
||||||
endin
|
|
||||||
|
|
||||||
instr 99
|
|
||||||
aL, aR freeverb gaReverb, gaReverb, 0.8, 0.5
|
|
||||||
outs aL, aR
|
|
||||||
gaReverb = 0
|
|
||||||
endin
|
|
||||||
|
|
||||||
; Start reverb (always on)
|
|
||||||
i 99 0 -1
|
|
||||||
|
|
||||||
|
|
||||||
i 1 0 2 440 0.3
|
|
||||||
|
|
||||||
; Arpeggio
|
|
||||||
i 1 0 0.5 261.63 0.2
|
|
||||||
i 1 0.5 0.5 329.63 0.2
|
|
||||||
i 1 1.0 0.5 392.00 0.2
|
|
||||||
i 1 1.5 0.5 523.25 0.2
|
|
||||||
|
|
||||||
; Bass line
|
|
||||||
i 2 0 0.5 130.81 0.4
|
|
||||||
i 2 0.5 0.5 146.83 0.4
|
|
||||||
i 2 1.0 0.5 164.81 0.4
|
|
||||||
i 2 1.5 0.5 130.81 0.4
|
|
||||||
|
|
||||||
; Long note for channel control
|
|
||||||
i 1 0 10 440 0.3
|
|
||||||
|
|
||||||
freq = 440
|
|
||||||
|
|
||||||
freq = 554.37
|
|
||||||
|
|
||||||
freq = 659.25
|
|
||||||
|
|
||||||
; Turn off instrument 1
|
|
||||||
i -1 0 0
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEMPLATE_REGISTRY: CsoundTemplate[] = [
|
|
||||||
EMPTY_TEMPLATE,
|
|
||||||
LIVECODING_TEMPLATE,
|
|
||||||
CLASSIC_TEMPLATE
|
|
||||||
];
|
|
||||||
|
|
||||||
export class TemplateRegistry {
|
|
||||||
private templates: Map<string, CsoundTemplate>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.templates = new Map(
|
|
||||||
TEMPLATE_REGISTRY.map(template => [template.id, template])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll(): CsoundTemplate[] {
|
|
||||||
return Array.from(this.templates.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
getById(id: string): CsoundTemplate | undefined {
|
|
||||||
return this.templates.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmpty(): CsoundTemplate {
|
|
||||||
return EMPTY_TEMPLATE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const templateRegistry = new TemplateRegistry();
|
|
||||||
Reference in New Issue
Block a user