├── .github └── workflows │ └── main.yml ├── .gitignore ├── .ocamlformat ├── LICENSE.md ├── README.md ├── bin ├── clock.ml ├── dune ├── event.ml ├── import.ml ├── list_devices.ml ├── main.ml ├── midi.ml ├── midi.mli ├── my_fpath.ml ├── my_fpath.mli ├── play.ml ├── simple_engine.ml ├── stat_engine.ml ├── util.ml └── watchdog.ml ├── cardio_crumble.opam ├── dune-project ├── test ├── cardio_crumble.ml └── dune └── test_executable ├── dune └── main.ml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Main workflow 3 | on: 4 | pull_request: null 5 | push: null 6 | schedule: 7 | - cron: 0 1 * * MON 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - macos-latest 15 | - ubuntu-latest 16 | ocaml-compiler: 17 | - 5.0.x 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 23 | uses: ocaml/setup-ocaml@v2 24 | with: 25 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 26 | - run: opam install . --deps-only --with-test 27 | - run: opam exec -- dune build 28 | - run: opam exec -- dune runtest 29 | lint-fmt: 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | os: 34 | - macos-latest 35 | - ubuntu-latest 36 | ocaml-compiler: 37 | - 5.0.x 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v3 42 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 43 | uses: ocaml/setup-ocaml@v2 44 | with: 45 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 46 | - name: Lint fmt 47 | uses: ocaml/setup-ocaml/lint-fmt@v2 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.install 3 | *.merlin 4 | _opam 5 | *.events 6 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.24.1 2 | profile=conventional 3 | parse-docstrings=true 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Enguerrand Decorne, Sonja Heinze 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cardio-crumble 2 | 3 | *Auditively feel the work of the runtime* 4 | 5 | Cardio-crumble is a program allowing you to listen to your program. 6 | Not listen as in `connect to your program using a previously established socket`, but literally, *listen with your ear*. 7 | 8 | It makes use of [runtime events](https://v2.ocaml.org/releases/5.0/api/Runtime_events.html), in order to gather various runtime statistics, and turns them in MIDI notes. 9 | These MIDI notes are then sent to your synthesizer of choice (hardware or software), for you to finally accurately profile your OCaml program. 10 | 11 | ## Quickstart 12 | 13 | Cardio-crumble is only compatible with OCaml releases starting from `5.0`. 14 | It should work fine under both Linux and MacOS. 15 | 16 | To install it, you first need to install [Portmidi](https://github.com/PortMidi/portmidi) through your system's package manager. 17 | 18 | ```shell 19 | # For Ubuntu 20 | sudo apt install libportmidi0 libportmidi-dev 21 | # For MacOS 22 | brew install portmidi 23 | ``` 24 | You can then clone `cardio-crumble`, and from the source directory run the following commands 25 | 26 | ```shell 27 | # Install the OPAM dependencies 28 | opam install . --deps-only -t 29 | # Build it! 30 | dune build 31 | ``` 32 | 33 | You now have a fully functional build of Cardio-crumble! Time to jam! 34 | 35 | ## How to use 36 | 37 | Cardio-crumble is an executable, that takes at least three arguments: 38 | 39 | - A device id: This represents a MIDI device connected to your system. 40 | - An engine name: There are two so called *engines* in cardio-crumble. An engine is best viewed as a way to procress events, and how to approach generating sequences of notes from them. 41 | - A path to an OCaml program: This is the program that cardio-crumble will listen to. This program needs to be built with OCaml 5.0, as runtime events were note available before this release. Otherwise there's no restriction! 42 | 43 | There is also a few optional parameters, each specific to each engines, in order to tune the behavior of `cardio-crumble`. 44 | 45 | ### Example run 46 | 47 | Beforehand, make sure you have a MIDI device correctly set up on your machine (be it software, or hardware.). 48 | 49 | If you don't have a MIDI device at hand, you can set up a software synthesizer. For that, refer to our wiki page: [MIDI Setup](https://github.com/pitag-ha/cardio-crumble/wiki/MIDI-Setup) 50 | 51 | 52 | Find a device id by listing all the MIDI devices on your system 53 | 54 | ```shell 55 | $ dune exec bin/main.exe list_devices 56 | number of devices: 2 57 | device 0 58 | name: HAPAX 59 | interface: CoreMIDI 60 | input: true 61 | output: false 62 | device 1 63 | name: HAPAX 64 | interface: CoreMIDI 65 | input: false 66 | output: true 67 | ``` 68 | 69 | You can see that in this example we have one `output` device (which is a device you can send MIDI data to). We will use it going forward (so `device_id` should be `1`!) 70 | 71 | Now you're all set and can try running `cardio-crumble`! 72 | ```shell 73 | dune exec -- bin/main.exe stat_engine --device_id=1 _build/default/test_executable/main.exe 74 | ``` 75 | 76 | We are now running `cardio-crumble` on a convenient test executable (located in `cardio-crumble`'s source tree), and if anything goes well, you should be hearing music from your synthesizer! 77 | 78 | Do note that if the program you are running cardio-crumble on needs to be passed parameters as well, you need to make sure you properly segment your command line call, so that `dune` knows to which program a parameter belongs: 79 | 80 | 81 | ```shell 82 | dune exec -- bin/main.exe stat_engine --device_id=1 --bpm=60 -- my_program -- my_program_parameters 83 | ``` 84 | 85 | ### Engines and optional parameters 86 | 87 | There are currently two engines in cardio-crumble: 88 | 89 | - `stat_engine` will aggregate MIDI events into a buffer, and will rhytmically output notes onto the MIDI device by measuring which events were the most important in a given time slice. 90 | - `simple_engine` will output events as they are processed, with a simple mapping. (one event received = one note emitted.) 91 | 92 | Each engines possess their own set of optional parameters to further tune the behavior of cardio-crumble (and as such, the amazing composition you are working on!). 93 | 94 | You can list there by running the `cardio-crumble` executable with an engine name, followed by the `--help` option. 95 | 96 | ```shell 97 | dune exec bin/main.exe -- stat_engine --help 98 | ``` 99 | 100 | Notable options: 101 | 102 | - `--scale` Allows you to select a musical [scale](https://en.wikipedia.org/wiki/Scale_(music)) to which `cardio-crumble` will stick to when generating notes. Available options are `minor`, `blue`, `major`, and the ~~...nice...~~ experimental `nice` scale. 103 | - `--bpm` (stat_engine only): Allows you to set the tempo that `cardio-crumble` will follow when playing notes. 104 | 105 | ## Demo! Cardio Dolphin Dreams about Mario, Bubbles and Coral Reefs 106 | 107 | 108 | This video is a recording of `cardio-crumble`, running on a pure OCaml GameBoy emulator ([BetterBoy](https://github.com/unsound-io/BetterBoy)). 109 | 110 | [![Cardio-crumble Demo](https://img.youtube.com/vi/fA9BdO2JyyE/0.jpg)](https://www.youtube.com/watch?v=fA9BdO2JyyE) 111 | -------------------------------------------------------------------------------- /bin/clock.ml: -------------------------------------------------------------------------------- 1 | type clock_source = Internal of int | External of int 2 | 3 | module CQueue = struct 4 | type t = { 5 | cond : Condition.t; 6 | mutex : Mutex.t; 7 | queue : (Midi.Note.t * int) Queue.t; 8 | } 9 | 10 | let create () = 11 | let queue = Queue.create () in 12 | let mutex = Mutex.create () in 13 | let cond = Condition.create () in 14 | { queue; cond; mutex } 15 | end 16 | 17 | let milestone : (Midi.Note.t option * int) Atomic.t = Atomic.make (None, 96) 18 | let clock_iterator = ref 0 19 | 20 | let process_event { CQueue.queue; cond; _ } device 21 | (ev : Portmidi.Portmidi_event.t) = 22 | let tick () = 23 | incr clock_iterator; 24 | let note, note_length = Atomic.get milestone in 25 | if !clock_iterator >= note_length then ( 26 | (match note with 27 | | Some note -> Midi.(write_output device [ message_off ~note () ]) 28 | | None -> ()); 29 | match Queue.take_opt queue with 30 | | None -> 31 | Condition.signal cond; 32 | Atomic.set milestone (None, 1) 33 | | Some (note, notes_per_beat) -> 34 | let cycles = 24 / notes_per_beat in 35 | Midi.(write_output device [ message_on ~note () ]); 36 | Atomic.set milestone (Some note, cycles); 37 | clock_iterator := 0; 38 | if Queue.is_empty queue then Condition.signal cond) 39 | in 40 | match ev.message with 41 | | 0xF8l -> tick () 42 | | 0xFCl -> clock_iterator := 0 43 | | _ -> () 44 | 45 | let external_main input_device_id output_device note_queue = 46 | let device = Midi.Device.create_input input_device_id in 47 | while not (Atomic.get Watchdog.terminate) do 48 | match Portmidi.read_input ~length:1 device with 49 | | Ok l -> List.iter (process_event note_queue output_device) l 50 | | Error _ -> print_endline "oh no" 51 | done; 52 | Condition.signal note_queue.CQueue.cond; 53 | match Portmidi.close_input device with 54 | | Error _ -> Printf.eprintf "Error while closing input device\n" 55 | | _ -> () 56 | 57 | let internal_main bpm device { CQueue.queue; cond; _ } = 58 | while not (Atomic.get Watchdog.terminate) do 59 | match Queue.take_opt queue with 60 | | None -> 61 | Condition.signal cond; 62 | Unix.sleepf 0.01 63 | | Some (note, n) -> 64 | Midi.(write_output device [ message_on ~note () ]); 65 | Unix.sleepf (60. /. float_of_int bpm /. float_of_int n); 66 | Midi.(write_output device [ message_off ~note () ]); 67 | if Queue.is_empty queue then Condition.signal cond 68 | done; 69 | Condition.signal cond 70 | 71 | let clock_func clock_source output_device note_queue () = 72 | match clock_source with 73 | | External input_device_id -> 74 | external_main input_device_id output_device note_queue 75 | | Internal bpm -> internal_main bpm output_device note_queue 76 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name cardio_crumble) 3 | (name main) 4 | (libraries runtime_events unix sexplib0 portmidi cmdliner fpath)) 5 | -------------------------------------------------------------------------------- /bin/event.ml: -------------------------------------------------------------------------------- 1 | open Runtime_events 2 | 3 | type t = Runtime_events.runtime_phase 4 | 5 | let compare ~f e1 e2 = if e1 = e2 then 0 else if f e1 > f e2 then 1 else -1 6 | 7 | let all = 8 | [ 9 | EV_EXPLICIT_GC_SET; 10 | EV_EXPLICIT_GC_STAT; 11 | EV_EXPLICIT_GC_MINOR; 12 | EV_EXPLICIT_GC_MAJOR; 13 | EV_EXPLICIT_GC_FULL_MAJOR; 14 | EV_EXPLICIT_GC_COMPACT; 15 | EV_MAJOR; 16 | EV_MAJOR_SWEEP; 17 | EV_MAJOR_MARK_ROOTS; 18 | EV_MAJOR_MARK; 19 | EV_MINOR; 20 | EV_MINOR_LOCAL_ROOTS; 21 | EV_MINOR_FINALIZED; 22 | EV_EXPLICIT_GC_MAJOR_SLICE; 23 | EV_FINALISE_UPDATE_FIRST; 24 | EV_FINALISE_UPDATE_LAST; 25 | EV_INTERRUPT_REMOTE; 26 | EV_MAJOR_EPHE_MARK; 27 | EV_MAJOR_EPHE_SWEEP; 28 | EV_MAJOR_FINISH_MARKING; 29 | EV_MAJOR_GC_CYCLE_DOMAINS; 30 | EV_MAJOR_GC_PHASE_CHANGE; 31 | EV_MAJOR_GC_STW; 32 | EV_MAJOR_MARK_OPPORTUNISTIC; 33 | EV_MAJOR_SLICE; 34 | EV_MAJOR_FINISH_CYCLE; 35 | EV_MINOR_CLEAR; 36 | EV_MINOR_FINALIZERS_OLDIFY; 37 | EV_MINOR_GLOBAL_ROOTS; 38 | EV_MINOR_LEAVE_BARRIER; 39 | EV_STW_API_BARRIER; 40 | EV_STW_HANDLER; 41 | EV_STW_LEADER; 42 | EV_MAJOR_FINISH_SWEEPING; 43 | EV_MINOR_FINALIZERS_ADMIN; 44 | EV_MINOR_REMEMBERED_SET; 45 | EV_MINOR_REMEMBERED_SET_PROMOTE; 46 | EV_MINOR_LOCAL_ROOTS_PROMOTE; 47 | EV_DOMAIN_CONDITION_WAIT; 48 | EV_DOMAIN_RESIZE_HEAP_RESERVATION; 49 | ] 50 | 51 | let event_to_int event = 52 | let rec loop i = function 53 | | x :: xs -> if x = event then i else loop (i + 1) xs 54 | | [] -> raise Not_found 55 | in 56 | loop 0 all 57 | -------------------------------------------------------------------------------- /bin/import.ml: -------------------------------------------------------------------------------- 1 | module Result = struct 2 | include Result 3 | 4 | module Syntax = struct 5 | let ( let+ ) x f = Result.map f x 6 | let ( let* ) x f = Result.bind x f 7 | 8 | let ( let*! ) : 'a 'b 'e. ('a, 'e) t -> ('a -> 'b) -> 'b = 9 | fun x f -> f (Result.get_ok x) 10 | 11 | let ( >>| ) x f = Result.map f x 12 | let ( >>= ) x f = Result.bind x f 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/list_devices.ml: -------------------------------------------------------------------------------- 1 | (* this function is extrated from the Portmidi bindings by Michael Bacarella. 2 | Licenced under LGPL-2.1-or-later with OCaml-LGPL-linking-exception 3 | See https://github.com/mbacarella/portmidi *) 4 | open Cmdliner 5 | 6 | let list_devices () = 7 | let num_devices = Portmidi.count_devices () in 8 | Printf.printf "number of devices: %d\n" num_devices; 9 | for i = 0 to pred num_devices do 10 | Printf.printf "device %d\n" i; 11 | match Portmidi.get_device_info i with 12 | | None -> Printf.printf "device %d not found\n" i 13 | | Some di -> 14 | Printf.printf " name: %s\n" 15 | (Option.value ~default:"null" di.Portmidi.Device_info.name); 16 | Printf.printf " interface: %s\n" 17 | (Option.value ~default:"null" di.Portmidi.Device_info.interface); 18 | Printf.printf " input: %B\n" di.Portmidi.Device_info.input; 19 | Printf.printf " output: %B\n" di.Portmidi.Device_info.output 20 | done; 21 | Portmidi.terminate (); 22 | 0 23 | 24 | let devices_t = Term.(const list_devices $ const ()) 25 | let cmd = Cmd.v (Cmd.info "list_devices") devices_t 26 | -------------------------------------------------------------------------------- /bin/main.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | Distributed under the ISC license, see terms at the end of the file. 4 | %%NAME%% %%VERSION%% 5 | ---------------------------------------------------------------------------*) 6 | 7 | open Cmdliner 8 | 9 | let cmds = [ List_devices.cmd; Simple_engine.cmd; Stat_engine.cmd ] 10 | 11 | (* Command line interface *) 12 | 13 | let doc = "Auditively feel the work of the runtime " 14 | let sdocs = Manpage.s_common_options 15 | let man = [ `S Manpage.s_description; `P "Listen to the OCaml runtime" ] 16 | 17 | let main = 18 | Cmd.group 19 | (Cmd.info "cardio-crumble" ~version:"%%VERSION%%" ~doc ~sdocs ~man) 20 | cmds 21 | 22 | let main () = Stdlib.exit @@ Cmd.eval' main 23 | let () = main () 24 | 25 | (*--------------------------------------------------------------------------- 26 | Copyright (c) 2016 Daniel C. Bünzli 27 | 28 | Permission to use, copy, modify, and/or distribute this software for any 29 | purpose with or without fee is hereby granted, provided that the above 30 | copyright notice and this permission notice appear in all copies. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 33 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 34 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 35 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 36 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 37 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 38 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 39 | ---------------------------------------------------------------------------*) 40 | -------------------------------------------------------------------------------- /bin/midi.ml: -------------------------------------------------------------------------------- 1 | open! Import 2 | open Result.Syntax 3 | module Event = Portmidi.Portmidi_event 4 | 5 | let error_to_string msg = 6 | Portmidi.Portmidi_error.sexp_of_t msg |> Sexplib0.Sexp.to_string 7 | 8 | let () = 9 | match Portmidi.initialize () with 10 | | Ok () -> () 11 | | Error _e -> failwith "error initializing portmidi" 12 | 13 | module Device = struct 14 | type output = { device_id : int; device : Portmidi.Output_stream.t } 15 | type input = Portmidi.Input_stream.t 16 | 17 | (* TODO: don't hardcode device_id. get it from the portmidi [get_device] *) 18 | let create_output device_id = 19 | match Portmidi.open_output ~device_id ~buffer_size:0l ~latency:1l with 20 | | Error _ -> 21 | Printf.eprintf "Can't find midi device with id: %i\nIs it connected?\n" 22 | device_id; 23 | exit 1 24 | (* let err = Printf.sprintf "Can't find midi device with id %i.Is it connected?" device_id in failwith err *) 25 | | Ok device -> { device; device_id } 26 | 27 | let create_input device_id = 28 | match Portmidi.open_input ~device_id ~buffer_size:1024l with 29 | | Error _ -> 30 | Printf.eprintf "Can't find midi device with id: %i\nIs it connected?\n" 31 | device_id; 32 | exit 1 33 | | Ok device -> device 34 | 35 | let turn_off_everything device_id = 36 | let device = create_output device_id in 37 | let* _ = 38 | Portmidi.write_output device.device 39 | [ 40 | Event.create ~status:'\176' ~data1:'\123' ~data2:'\000' ~timestamp:0l; 41 | ] 42 | in 43 | Portmidi.close_output device.device 44 | 45 | let shutdown { device; device_id } = 46 | let* _ = Portmidi.close_output device in 47 | Unix.sleepf 0.5; 48 | turn_off_everything device_id 49 | end 50 | 51 | module Note = struct 52 | type t = { pitch : char; volume : char } 53 | end 54 | 55 | let channel = ref 0 56 | 57 | let message_on ~note ?(timestamp = 0l) () = 58 | let { Note.pitch; volume } = note in 59 | let channel = 15 land !channel in 60 | let status = char_of_int (144 lor channel) in 61 | Event.create ~status ~data1:pitch ~data2:volume ~timestamp 62 | 63 | let message_off ~note ?(timestamp = 0l) () = 64 | let { Note.pitch; volume } = note in 65 | let channel = 15 land !channel in 66 | let status = char_of_int (128 lor channel) in 67 | Event.create ~status ~data1:pitch ~data2:volume ~timestamp 68 | 69 | let bend_pitch ~bend ?(timestamp = 0l) () = 70 | let channel = 15 land !channel in 71 | let status = char_of_int (224 lor channel) in 72 | let data1 = char_of_int (bend land 0b1111111) in 73 | let data2 = char_of_int (bend lsr 7) in 74 | Event.create ~status ~data1 ~data2 ~timestamp 75 | 76 | let control_change ~cc ~value ?(timestamp = 0l) () = 77 | if cc > 119 then invalid_arg "Sorry, [cc] must be <= 119" 78 | else 79 | let data1 = char_of_int (cc land 0b1111111) in 80 | let data2 = char_of_int (value land 0b1111111) in 81 | Event.create ~status:'\176' ~data1 ~data2 ~timestamp 82 | 83 | (* Best function ever!! <3 *) 84 | let handle_error = function Ok _ -> () | Error _ -> () 85 | 86 | let write_output { Device.device; _ } msg = 87 | Portmidi.write_output device msg |> handle_error 88 | 89 | module Scale = struct 90 | type t = Major | Minor | Pentatonic | Nice | Blue | Overtones 91 | 92 | let partition note_as_int = 93 | (* 94 | 0 -> (0, 0) 95 | 1 -> (0,1) 96 | (...) 97 | 6 -> (0,6) 98 | 7 -> (1,0) 99 | 8 -> (1,1) 100 | *) 101 | let scale_func = note_as_int mod 7 in 102 | let octave = (note_as_int - scale_func) / 7 in 103 | (octave, scale_func) 104 | 105 | let major base_note i = 106 | let octave, scale_func = partition i in 107 | match scale_func with 108 | | 0 -> 109 | { 110 | Note.pitch = Char.chr @@ (base_note + (12 * octave)); 111 | volume = '\090'; 112 | } 113 | | 1 -> 114 | { pitch = Char.chr @@ (base_note + 2 + (12 * octave)); volume = '\070' } 115 | | 2 -> 116 | { pitch = Char.chr @@ (base_note + 4 + (12 * octave)); volume = '\070' } 117 | | 3 -> 118 | { pitch = Char.chr @@ (base_note + 5 + (12 * octave)); volume = '\070' } 119 | | 4 -> 120 | { pitch = Char.chr @@ (base_note + 7 + (12 * octave)); volume = '\070' } 121 | | 5 -> 122 | { pitch = Char.chr @@ (base_note + 9 + (12 * octave)); volume = '\070' } 123 | | 6 -> 124 | { 125 | pitch = Char.chr @@ (base_note + 11 + (12 * octave)); 126 | volume = '\070'; 127 | } 128 | | _ -> 129 | failwith 130 | "Why on earth is something mod 7 not element of {0,1,2,3,4,5,6}?" 131 | 132 | let minor base_note i = 133 | let octave, scale_func = partition i in 134 | match scale_func with 135 | | 0 -> 136 | { 137 | Note.pitch = Char.chr @@ (base_note + (12 * octave)); 138 | volume = '\090'; 139 | } 140 | | 1 -> 141 | { pitch = Char.chr @@ (base_note + 2 + (12 * octave)); volume = '\070' } 142 | | 2 -> 143 | { pitch = Char.chr @@ (base_note + 3 + (12 * octave)); volume = '\070' } 144 | | 3 -> 145 | { pitch = Char.chr @@ (base_note + 5 + (12 * octave)); volume = '\070' } 146 | | 4 -> 147 | { pitch = Char.chr @@ (base_note + 7 + (12 * octave)); volume = '\070' } 148 | | 5 -> 149 | { pitch = Char.chr @@ (base_note + 8 + (12 * octave)); volume = '\070' } 150 | | 6 -> 151 | { 152 | pitch = Char.chr @@ (base_note + 10 + (12 * octave)); 153 | volume = '\070'; 154 | } 155 | | _ -> 156 | failwith 157 | "Why on earth is something mod 7 not element of {0,1,2,3,4,5,6}?" 158 | 159 | let pentatonic base_note i = 160 | let octave, scale_func = partition i in 161 | match scale_func with 162 | | 0 -> 163 | { 164 | Note.pitch = Char.chr @@ (base_note + (12 * octave)); 165 | volume = '\090'; 166 | } 167 | | 1 -> 168 | { pitch = Char.chr @@ (base_note + 2 + (12 * octave)); volume = '\070' } 169 | | 2 -> 170 | { pitch = Char.chr @@ (base_note + 4 + (12 * octave)); volume = '\070' } 171 | | 3 -> 172 | { pitch = Char.chr @@ (base_note + 7 + (12 * octave)); volume = '\070' } 173 | | 4 -> 174 | { pitch = Char.chr @@ (base_note + 9 + (12 * octave)); volume = '\070' } 175 | | 5 -> 176 | { 177 | pitch = Char.chr @@ (base_note + 12 + (12 * octave)); 178 | volume = '\070'; 179 | } 180 | | 6 -> 181 | { 182 | pitch = Char.chr @@ (base_note + 14 + (12 * octave)); 183 | volume = '\070'; 184 | } 185 | | _ -> 186 | failwith 187 | "Why on earth is something mod 7 not element of {0,1,2,3,4,5,6}?" 188 | 189 | let nice_scale base_note i = 190 | let octave, scale_func = partition i in 191 | match scale_func with 192 | | 0 -> 193 | { 194 | Note.pitch = Char.chr @@ (base_note + (12 * octave)); 195 | volume = '\090'; 196 | } 197 | | 1 -> 198 | { pitch = Char.chr @@ (base_note + 2 + (12 * octave)); volume = '\070' } 199 | | 2 -> 200 | { pitch = Char.chr @@ (base_note + 3 + (12 * octave)); volume = '\070' } 201 | | 3 -> 202 | { pitch = Char.chr @@ (base_note + 4 + (12 * octave)); volume = '\070' } 203 | | 4 -> 204 | { pitch = Char.chr @@ (base_note + 7 + (12 * octave)); volume = '\070' } 205 | | 5 -> 206 | { pitch = Char.chr @@ (base_note + 9 + (12 * octave)); volume = '\070' } 207 | | 6 -> 208 | { 209 | pitch = Char.chr @@ (base_note + 12 + (12 * octave)); 210 | volume = '\070'; 211 | } 212 | | _ -> 213 | failwith 214 | "Why on earth is something mod 7 not element of {0,1,2,3,4,5,6}?" 215 | 216 | let blue base_note i = 217 | let octave, scale_func = partition i in 218 | match scale_func with 219 | | 0 -> 220 | { 221 | Note.pitch = Char.chr @@ (base_note + (12 * octave)); 222 | volume = '\090'; 223 | } 224 | | 1 -> 225 | { pitch = Char.chr @@ (base_note + 3 + (12 * octave)); volume = '\070' } 226 | | 2 -> 227 | { pitch = Char.chr @@ (base_note + 5 + (12 * octave)); volume = '\070' } 228 | | 3 -> 229 | { pitch = Char.chr @@ (base_note + 6 + (12 * octave)); volume = '\070' } 230 | | 4 -> 231 | { pitch = Char.chr @@ (base_note + 7 + (12 * octave)); volume = '\070' } 232 | | 5 -> 233 | { 234 | pitch = Char.chr @@ (base_note + 10 + (12 * octave)); 235 | volume = '\070'; 236 | } 237 | | 6 -> 238 | { 239 | pitch = Char.chr @@ (base_note + 12 + (12 * octave)); 240 | volume = '\070'; 241 | } 242 | | _ -> 243 | failwith 244 | "Why on earth is something mod 7 not element of {0,1,2,3,4,5,6}?" 245 | 246 | let overtones base_note i = 247 | let octave, scale_func = partition i in 248 | match scale_func with 249 | | 0 -> 250 | { 251 | Note.pitch = Char.chr @@ (base_note + (12 * octave)); 252 | volume = '\090'; 253 | } 254 | | 1 -> 255 | { 256 | pitch = Char.chr @@ (base_note + 12 + (12 * octave)); 257 | volume = '\070'; 258 | } 259 | | 2 -> 260 | { 261 | pitch = Char.chr @@ (base_note + 19 + (12 * octave)); 262 | volume = '\070'; 263 | } 264 | | 3 -> 265 | { 266 | pitch = Char.chr @@ (base_note + 31 + (12 * octave)); 267 | volume = '\070'; 268 | } 269 | | 4 -> 270 | { 271 | pitch = Char.chr @@ (base_note + 35 + (12 * octave)); 272 | volume = '\070'; 273 | } 274 | | 5 -> 275 | { pitch = Char.chr @@ (base_note + (12 * octave)); volume = '\070' } 276 | (*FIXME*) 277 | | 6 -> 278 | { pitch = Char.chr @@ (base_note + (12 * octave)); volume = '\070' } 279 | (*FIXME*) 280 | | _ -> 281 | failwith 282 | "Why on earth is something mod 7 not element of {0,1,2,3,4,5,6}?" 283 | 284 | let get ~base_note = function 285 | | Nice -> nice_scale base_note 286 | | Blue -> blue base_note 287 | | Major -> major base_note 288 | | Minor -> minor base_note 289 | | Pentatonic -> pentatonic base_note 290 | | Overtones -> overtones base_note 291 | end 292 | -------------------------------------------------------------------------------- /bin/midi.mli: -------------------------------------------------------------------------------- 1 | module Event = Portmidi.Portmidi_event 2 | 3 | val error_to_string : Portmidi.Portmidi_error.t -> string 4 | 5 | module Device : sig 6 | type output 7 | type input = Portmidi.Input_stream.t 8 | 9 | val create_output : int -> output 10 | val create_input : int -> input 11 | val shutdown : output -> (unit, Portmidi.Portmidi_error.t) result 12 | end 13 | 14 | val channel : int ref 15 | 16 | module Note : sig 17 | type t = { pitch : char; volume : char } 18 | end 19 | 20 | val message_on : note:Note.t -> ?timestamp:int32 -> unit -> Event.t 21 | val message_off : note:Note.t -> ?timestamp:int32 -> unit -> Event.t 22 | val bend_pitch : bend:int -> ?timestamp:int32 -> unit -> Event.t 23 | 24 | val control_change : cc:int -> value:int -> ?timestamp:int32 -> unit -> Event.t 25 | (** This helps to send MIDI Control Change messages 26 | 27 | @raise Invalid_argument if [cc] is greater than 119 *) 28 | 29 | val write_output : Device.output -> Portmidi.Portmidi_event.t list -> unit 30 | 31 | module Scale : sig 32 | type t = Major | Minor | Pentatonic | Nice | Blue | Overtones 33 | 34 | val get : base_note:int -> t -> int -> Note.t 35 | end 36 | -------------------------------------------------------------------------------- /bin/my_fpath.ml: -------------------------------------------------------------------------------- 1 | include Fpath 2 | 3 | let handle_result = function 4 | | Error (`Msg msg) -> 5 | let s = Printf.sprintf "Error from Fpath: %s" msg in 6 | failwith s 7 | | Ok x -> x 8 | -------------------------------------------------------------------------------- /bin/my_fpath.mli: -------------------------------------------------------------------------------- 1 | include module type of Fpath 2 | 3 | val handle_result : (t, [< `Msg of string ]) result -> t 4 | -------------------------------------------------------------------------------- /bin/play.ml: -------------------------------------------------------------------------------- 1 | let event_to_note tones event = tones (Event.event_to_int event) 2 | 3 | let handle_control_c () = 4 | let handle = 5 | Sys.Signal_handle (fun _ -> Atomic.set Watchdog.terminate true) 6 | in 7 | Sys.(signal sigint handle) 8 | 9 | let play ~tracing midi_out channel scale argv = 10 | Midi.channel := channel - 1; 11 | let dir, pid, child_alive = 12 | match argv with 13 | | first_arg :: args -> 14 | let dir, file = 15 | My_fpath.(split_base @@ handle_result @@ of_string first_arg) 16 | in 17 | if String.equal (My_fpath.get_ext file) ".events" then 18 | let pid = int_of_string @@ My_fpath.(to_string @@ rem_ext file) in 19 | (My_fpath.to_string dir, pid, fun () -> true) 20 | else 21 | let args = Array.of_list args in 22 | let pid = 23 | Unix.create_process_env first_arg args 24 | [| "OCAML_RUNTIME_EVENTS_START=1" |] 25 | Unix.stdin Unix.stdout Unix.stderr 26 | in 27 | (".", pid, Util.child_alive pid) 28 | | _ -> 29 | failwith 30 | "cardio-crumble expects a positional argument. It can be either the \ 31 | path to the executable you want cardio-crumble to run or the path \ 32 | to the event ring of a running process. In the latter case, the \ 33 | process has to be spawned with OCAML_RUNTIME_EVENTS_START=1 :)" 34 | in 35 | let device = Midi.Device.create_output midi_out in 36 | let _ = handle_control_c () in 37 | Unix.sleepf 0.1; 38 | tracing device child_alive 39 | (Some (dir, pid)) 40 | (Midi.Scale.get ~base_note:48 scale); 41 | print_endline "got to the end"; 42 | match Midi.Device.shutdown device with 43 | | Ok () -> 0 44 | | Error msg -> 45 | let s = Midi.error_to_string msg in 46 | Printf.eprintf "Error during device shutdown: %s" s; 47 | 1 48 | 49 | open Cmdliner 50 | 51 | let argv = Arg.(non_empty & pos_all string [] & info [] ~docv:"ARGV") 52 | 53 | let midi_out = 54 | Arg.(value & opt int 0 & info [ "o"; "midi-out" ] ~docv:"DEVICE_ID") 55 | 56 | let channel = Arg.(value & opt int 1 & info [ "c"; "channel" ] ~docv:"CHANNEL") 57 | 58 | let scale_enum = 59 | Arg.enum 60 | [ 61 | ("nice", Midi.Scale.Nice); 62 | ("major", Midi.Scale.Major); 63 | ("minor", Midi.Scale.Minor); 64 | ("pentatonic", Midi.Scale.Pentatonic); 65 | ("blue", Midi.Scale.Blue); 66 | ("overtones", Midi.Scale.Overtones); 67 | ] 68 | 69 | let scale = 70 | Arg.( 71 | value & opt scale_enum Midi.Scale.Nice & info [ "s"; "scale" ] ~docv:"SCALE") 72 | -------------------------------------------------------------------------------- /bin/simple_engine.ml: -------------------------------------------------------------------------------- 1 | open Runtime_events 2 | open Cmdliner 3 | 4 | let starting_time = ref None 5 | 6 | let adjust_time ts = 7 | (* The ints64 representing the duration of a runtime phase are in units of nanoseconds, whereas the ints32 representing the timestamps of midi notes are in units of miliseconds. 8 | So this function indirectly multiplies the timestamp by a a factor 1000 (intentionally). *) 9 | let int64_to_32 i = Int32.of_int @@ Int64.to_int @@ i in 10 | Option.map 11 | (fun st -> 12 | Int64.sub (Timestamp.to_int64 ts) (Timestamp.to_int64 st) |> int64_to_32) 13 | !starting_time 14 | 15 | let runtime_counter device tones _domain_id ts counter _value = 16 | match counter with 17 | | EV_C_MINOR_PROMOTED -> 18 | starting_time := Some ts; 19 | Midi.( 20 | let note = tones 0 in 21 | write_output device [ message_on ~note () ]) 22 | (* Unix.sleep 5; 23 | Midi.(write_output [ message_off ~note:base_note () ]) *) 24 | | _ -> () 25 | 26 | let runtime_begin device tones _domain_id ts event = 27 | let note = Play.event_to_note tones event in 28 | match adjust_time ts with 29 | | None -> () 30 | | Some ts -> 31 | Midi.(write_output device [ message_off ~note ~timestamp:ts () ]); 32 | Printf.printf "%f: start of %s. ts: %ld\n%!" (Sys.time ()) 33 | (Runtime_events.runtime_phase_name event) 34 | ts 35 | 36 | let runtime_end device tones _domain_id ts event = 37 | let note = Play.event_to_note tones event in 38 | match adjust_time ts with 39 | | None -> () 40 | | Some ts -> 41 | Midi.(write_output device [ message_off ~note ~timestamp:ts () ]); 42 | Printf.printf "%f: start of %s. ts: %ld\n%!" (Sys.time ()) 43 | (Runtime_events.runtime_phase_name event) 44 | ts 45 | 46 | let tracing device child_alive path_pid tones = 47 | let c = create_cursor path_pid in 48 | let runtime_begin = runtime_begin device tones in 49 | let runtime_end = runtime_end device tones in 50 | let runtime_counter = runtime_counter device tones in 51 | let cbs = Callbacks.create ~runtime_begin ~runtime_end ~runtime_counter () in 52 | let watchdog_domain = Domain.spawn (Watchdog.watchdog_func child_alive) in 53 | while not (Atomic.get Watchdog.terminate) do 54 | ignore (read_poll c cbs None); 55 | Unix.sleepf 0.1 56 | done; 57 | Domain.join watchdog_domain 58 | 59 | let simple_play = Play.play ~tracing 60 | 61 | let play_t = 62 | Term.( 63 | const simple_play $ Play.midi_out $ Play.channel $ Play.scale $ Play.argv) 64 | 65 | let cmd = Cmd.v (Cmd.info "simple_engine") play_t 66 | -------------------------------------------------------------------------------- /bin/stat_engine.ml: -------------------------------------------------------------------------------- 1 | open Runtime_events 2 | open Cmdliner 3 | 4 | let event_table : (Runtime_events.runtime_phase, int) Hashtbl.t = 5 | Hashtbl.create 32 6 | 7 | let event_table_lock = Mutex.create () 8 | 9 | let quantifier_table : (Runtime_events.runtime_phase, int) Hashtbl.t = 10 | Hashtbl.create 32 11 | 12 | let quantifier_table_lock = Mutex.create () 13 | 14 | let add_to_hashtbl tbl lock event = 15 | Mutex.lock lock; 16 | (match Hashtbl.find_opt tbl event with 17 | | Some v -> Hashtbl.add tbl event (v + 1) 18 | | None -> Hashtbl.add tbl event 1); 19 | Mutex.unlock lock 20 | 21 | let runtime_begin _domain_id _ts event = 22 | add_to_hashtbl event_table event_table_lock event; 23 | add_to_hashtbl quantifier_table quantifier_table_lock event 24 | 25 | let polling_func path_pid _ = 26 | let c = create_cursor path_pid in 27 | let cbs = Callbacks.create ~runtime_begin () in 28 | while not (Atomic.get Watchdog.terminate) do 29 | ignore (read_poll c cbs None) 30 | (* FIXME: Probably we want to sleep at least a bit *) 31 | (* Unix.sleepf 0.01 *) 32 | done 33 | 34 | let threshold = function 35 | | 0 -> -20. 36 | | 1 -> 0. 37 | | 2 -> 10. 38 | | 3 -> 20. 39 | | 4 -> 40. 40 | | 5 -> 50. 41 | | _ -> 42 | Format.printf 43 | "muahahahahhaha (won't start enumerating at 1 again ;)) \n%!"; 44 | exit 1 45 | 46 | let get_increment num_beats event = 47 | Mutex.lock event_table_lock; 48 | Mutex.lock quantifier_table_lock; 49 | let incr = 50 | match 51 | ( Hashtbl.find_opt event_table event, 52 | Hashtbl.find_opt quantifier_table event ) 53 | with 54 | | None, _ -> Float.neg_infinity 55 | | Some _, None -> failwith "quantifier table hasn't been updated" 56 | | Some num_this_beat, Some all_so_far -> 57 | let average = Float.of_int all_so_far /. Float.of_int num_beats in 58 | 100. -. (100. /. average *. Float.of_int num_this_beat) 59 | in 60 | Mutex.unlock event_table_lock; 61 | Mutex.unlock quantifier_table_lock; 62 | incr 63 | 64 | let sequencer_func num_beats tones _device _bpm queue _ = 65 | (* FIXME *) 66 | Mutex.lock queue.Clock.CQueue.mutex; 67 | let rec aux num_beats = 68 | let interesting_stuff = 69 | let compare e1 e2 = 70 | Int.neg (Event.compare ~f:(get_increment num_beats) e1 e2) 71 | in 72 | let sorted_events = List.sort compare Event.all in 73 | 74 | let rec loop acc = function 75 | | hd :: tl -> 76 | let i = List.length acc in 77 | if i = 6 then acc 78 | else 79 | let new_acc = 80 | if get_increment num_beats hd > threshold i then Some hd :: acc 81 | else None :: acc 82 | in 83 | loop new_acc tl 84 | | [] -> acc 85 | in 86 | loop [] sorted_events 87 | in 88 | let n = 89 | List.fold_left 90 | (fun acc -> function Some _ -> acc + 1 | None -> acc) 91 | 0 interesting_stuff 92 | in 93 | List.iter 94 | (function 95 | | None -> () 96 | | Some event -> 97 | let note = Play.event_to_note tones event in 98 | (* Debug: Adjust threashold: Currently, it's almost always pushing 6 nots or no notes. *) 99 | (* Printf.printf "Pushing a note with rythm %n to the queue\n%!" n; *) 100 | Queue.push (note, n) queue.Clock.CQueue.queue) 101 | interesting_stuff; 102 | Mutex.lock event_table_lock; 103 | Hashtbl.clear event_table; 104 | Mutex.unlock event_table_lock; 105 | Condition.wait queue.Clock.CQueue.cond queue.mutex; 106 | if Atomic.get Watchdog.terminate then () else aux (num_beats + 1) 107 | in 108 | aux num_beats 109 | 110 | let tracing midi_in bpm device child_alive path_pid tones = 111 | let queue = Clock.CQueue.create () in 112 | let clock_source = 113 | match (midi_in, bpm) with 114 | | None, None -> 115 | print_endline 116 | "No bpm or clock source given, using internal clock at 120 BPM"; 117 | Clock.Internal 120 118 | | None, Some bpm -> Internal bpm 119 | | Some input_device_id, bpm -> 120 | if Option.is_some bpm then 121 | print_endline 122 | "Ignoring the bpm argument since an external clock source was \ 123 | provided."; 124 | Clock.External input_device_id 125 | in 126 | let polling_domain = Domain.spawn (polling_func path_pid) in 127 | let sequencer_domain = 128 | Domain.spawn (sequencer_func 1 tones device bpm queue) 129 | in 130 | let watchdog_domain = Domain.spawn (Watchdog.watchdog_func child_alive) in 131 | let clock_domain = 132 | Domain.spawn (Clock.clock_func clock_source device queue) 133 | in 134 | List.iter Domain.join 135 | [ watchdog_domain; polling_domain; sequencer_domain; clock_domain ] 136 | 137 | let bpm = 138 | Arg.(value & opt (some int) None & info [ "bpm"; "--bpm" ] ~docv:"BPM") 139 | 140 | let midi_in = 141 | Arg.( 142 | value 143 | & opt (some int) None 144 | & info [ "i"; "midi-in" ] ~docv:"EXTERNAL_CLOCK_ID") 145 | 146 | let stat_play bpm external_clock_id = 147 | Play.play ~tracing:(tracing bpm external_clock_id) 148 | 149 | let play_t = 150 | Term.( 151 | const stat_play $ midi_in $ bpm $ Play.midi_out $ Play.channel $ Play.scale 152 | $ Play.argv) 153 | 154 | let cmd = Cmd.v (Cmd.info "stat_engine") play_t 155 | -------------------------------------------------------------------------------- /bin/util.ml: -------------------------------------------------------------------------------- 1 | (* this file is imported from Patrick Ferris's work at https://github.com/patricoferris/runtime-events-demo/ *) 2 | let child_alive child_pid () = 3 | match Unix.waitpid [ Unix.WNOHANG ] child_pid with 4 | | 0, _ -> true 5 | | p, _ when p = child_pid -> false 6 | | _, _ -> assert false 7 | -------------------------------------------------------------------------------- /bin/watchdog.ml: -------------------------------------------------------------------------------- 1 | (* When set to true, all reading threads should stop. *) 2 | let terminate = Atomic.make false 3 | 4 | (* 5 | The watchdog will periodically check that the child process is still alive. 6 | If the child process is gone, then it will set the terminate atomic variable to true. 7 | Threads should be checking for this atomic variable periodically, and shut down gracefully. 8 | Note: It is also possible that the terminate variable is set by a signal handler. 9 | *) 10 | let rec watchdog_func child_alive () = 11 | Unix.sleepf 0.1; 12 | match Atomic.get terminate with 13 | | true -> () 14 | | false -> 15 | if not (child_alive ()) then Atomic.set terminate true 16 | else watchdog_func child_alive () 17 | -------------------------------------------------------------------------------- /cardio_crumble.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "A short synopsis" 4 | description: "A longer description" 5 | maintainer: ["Maintainer Name"] 6 | authors: ["Author Name"] 7 | license: "LICENSE" 8 | tags: ["topics" "to describe" "your" "project"] 9 | homepage: "https://github.com/username/reponame" 10 | doc: "https://url/to/documentation" 11 | bug-reports: "https://github.com/username/reponame/issues" 12 | depends: [ 13 | "ocaml" {>= "5.0.0"} 14 | "dune" {>= "3.3"} 15 | "portmidi" {>= "0.1"} 16 | "cmdliner" {>= "1.1.1"} 17 | "odoc" {with-doc} 18 | ] 19 | build: [ 20 | ["dune" "subst"] {dev} 21 | [ 22 | "dune" 23 | "build" 24 | "-p" 25 | name 26 | "-j" 27 | jobs 28 | "@install" 29 | "@runtest" {with-test} 30 | "@doc" {with-doc} 31 | ] 32 | ] 33 | dev-repo: "git+https://github.com/username/reponame.git" 34 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.3) 2 | 3 | (name cardio_crumble) 4 | 5 | (generate_opam_files true) 6 | 7 | (source 8 | (github username/reponame)) 9 | 10 | (authors "Author Name") 11 | 12 | (maintainers "Maintainer Name") 13 | 14 | (license LICENSE) 15 | 16 | (documentation https://url/to/documentation) 17 | 18 | (package 19 | (name cardio_crumble) 20 | (synopsis "A short synopsis") 21 | (description "A longer description") 22 | (depends 23 | (ocaml 24 | (>= 5.0.0)) 25 | dune 26 | (portmidi 27 | (>= 0.1)) 28 | (cmdliner 29 | (>= 1.1.1)) 30 | ) 31 | (tags 32 | (topics "to describe" your project))) 33 | 34 | ; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project 35 | -------------------------------------------------------------------------------- /test/cardio_crumble.ml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pitag-ha/cardio-crumble/33f3caf199bc3c678c1f9caaf1f50f7167bbf82d/test/cardio_crumble.ml -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name cardio_crumble)) 3 | -------------------------------------------------------------------------------- /test_executable/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name some_loop) 3 | (name main) 4 | (libraries unix)) 5 | -------------------------------------------------------------------------------- /test_executable/main.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | while true do 3 | let _ = String.make 16 'a' in 4 | Gc.minor () 5 | done; 6 | print_endline "Hello, World!" 7 | --------------------------------------------------------------------------------