May 12May 12 Hello everyone,I would like to share a small utility function I recently developed for Opusmodus that may be useful for people working with long-form generative or multi-section compositions.The idea is very simple:The function automatically captures the output of pprint-last-score, creates a file if necessary, and appends the generated def-score into that file while allowing automatic or manual section naming.For example:(save-current-section :file "/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp")Automatically generates:(def-score section001 ...)(def-score section002 ...)(def-score section003 ...)Or with a custom name:(save-current-section :file "/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp" :name 'coda)One particularly useful aspect is the possibility to store metadata alongside each section:(save-current-section :file "/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp" :metadata `((:seed ,*gseed*) (:tempo 72) (:mode octatonic) (:algorithm score212)))This allows the score file to become not only a collection of sections, but also a compositional archive containing informations on each section.I have found this especially useful for:large algorithmic works,iterative composition,and building long-form modular pieces.The function automatically:creates the file if needed,appends new sections without overwriting,detects the next available section number,renames the internal def-score,and optionally stores metadata comments.I am sharing the full code below in case it may be useful to others.(in-package :opusmodus);;; ------------------------------------------------------------;;; CAPTURE DU DERNIER SCORE;;; ------------------------------------------------------------(defun capture-pprint-last-score () "Capture le texte produit par pprint-last-score sans la ligne pprint-last-score." (let* ((txt (with-output-to-string (*standard-output*) (pprint-last-score))) (pos (search "(def-score" txt :test #'char-equal))) (unless pos (error "Aucun (def-score ...) trouvé dans pprint-last-score.")) (subseq txt pos)));;; ------------------------------------------------------------;;; REMPLACEMENT DU NOM DU DEF-SCORE;;; ------------------------------------------------------------(defun replace-def-score-name (score-string new-name) "Remplace le nom du premier def-score par NEW-NAME." (let* ((new-name-string (string-downcase (string new-name))) (pos (search "(def-score" score-string :test #'char-equal))) (unless pos (error "Aucun (def-score ...) trouvé dans le texte capturé.")) (let* ((start (+ pos (length "(def-score"))) (after-space (position-if-not #'(lambda (c) (member c '(#\Space #\Tab #\Newline))) score-string :start start)) (end-name (position-if #'(lambda (c) (member c '(#\Space #\Tab #\Newline #\())) score-string :start after-space))) (concatenate 'string (subseq score-string 0 after-space) new-name-string (subseq score-string end-name)))));;; ------------------------------------------------------------;;; UTILITAIRES;;; ------------------------------------------------------------(defun read-file-as-string (file) "Lit FILE sous forme de string. Renvoie une string vide si le fichier n'existe pas." (if (probe-file file) (with-open-file (in file :direction :input) (let ((contents (make-string (file-length in)))) (read-sequence contents in) contents)) ""))(defun zero-pad-number (number width) "Convertit NUMBER en string avec des zéros à gauche." (let ((s (write-to-string number))) (concatenate 'string (make-string (max 0 (- width (length s))) :initial-element #\0) s)))(defun collect-auto-section-numbers (text prefix) "Collecte tous les numéros trouvés dans des noms du type PREFIX001, PREFIX002, etc." (let* ((prefix-string (string-downcase prefix)) (prefix-length (length prefix-string)) (text-lower (string-downcase text)) (numbers '()) (start 0)) (loop for pos = (search prefix-string text-lower :start2 start) while pos do (let* ((num-start (+ pos prefix-length)) (num-end num-start)) (loop while (and (< num-end (length text-lower)) (digit-char-p (char text-lower num-end))) do (incf num-end)) (when (> num-end num-start) (push (parse-integer text-lower :start num-start :end num-end) numbers)) (setf start num-end))) numbers))(defun next-section-number (file prefix) "Trouve le prochain numéro disponible pour PREFIX dans FILE." (let* ((text (read-file-as-string file)) (numbers (collect-auto-section-numbers text prefix))) (if numbers (1+ (apply #'max numbers)) 1)))(defun make-auto-section-name (file prefix digits) "Crée un nom automatique du type section001." (intern (string-upcase (format nil "~a~a" prefix (zero-pad-number (next-section-number file prefix) digits)))));;; ------------------------------------------------------------;;; FONCTION PRINCIPALE;;; ------------------------------------------------------------(defun save-current-section (&key file name (prefix "section") (digits 3) (separator t) (comment-date nil) metadata) "Sauvegarde le dernier score Opusmodus dans FILE.Si NAME est fourni, le def-score prendra ce nom.Si NAME est NIL, un nom automatique sera généré :section001, section002, section003, etc.Exemples : (save-current-section :file \"/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp\") (save-current-section :file \"/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp\" :name 'introduction) (save-current-section :file \"/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp\" :prefix \"part\" :digits 2) (save-current-section :file \"/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp\" :metadata '((:seed 12345) (:mode octatonic-1) (:tempo 72)))" (unless file (error "Tu dois fournir un chemin avec :file.")) (let* ((score-name (or name (make-auto-section-name file prefix digits))) (raw-score (capture-pprint-last-score)) (renamed-score (replace-def-score-name raw-score score-name))) (ensure-directories-exist file) (with-open-file (out file :direction :output :if-exists :append :if-does-not-exist :create) (when separator (format out "~%~%;;; ------------------------------------------------------------~%") (format out ";;; SCORE: ~a~%" score-name) (when comment-date (format out ";;; Exported: ~a~%" (get-universal-time))) (when metadata (format out ";;; Metadata: ~s~%" metadata)) (format out ";;; ------------------------------------------------------------~%~%")) (format out "~a~%" renamed-score)) score-name))#|USAGEUtilisation automatique :(save-current-section :file "/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp")Cela écrit automatiquement :(def-score section001 ...)Puis au prochain appel :(def-score section002 ...)Avec un nom manuel :(save-current-section :file "/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp" :name 'coda)Avec métadonnées :(save-current-section :file "/Users/stephaneboussuge/Opusmodus/Scores/my-piece.lisp" :metadata `((:seed ,*gseed*) (:tempo 72) (:material "octatonic")))|#Best,Stéphane
Create an account or sign in to comment