• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mbarbin / fingerboard / 118

23 Dec 2025 10:30PM UTC coverage: 92.134% (-3.0%) from 95.087%
118

Pull #11

github

web-flow
Merge b17e74f06 into ccb9da732
Pull Request #11: Reduce deps

323 of 528 new or added lines in 29 files covered. (61.17%)

1 existing line in 1 file now uncovered.

3830 of 4157 relevant lines covered (92.13%)

20265.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.15
/src/system.ml
1
(**********************************************************************************)
2
(*  Fingerboard - a microtonal geography of the cello fingerboard                 *)
3
(*  Copyright (C) 2022-2024 Mathieu Barbin <mathieu.barbin@gmail.com>             *)
4
(*                                                                                *)
5
(*  This file is part of Fingerboard.                                             *)
6
(*                                                                                *)
7
(*  Fingerboard is free software: you can redistribute it and/or modify it under  *)
8
(*  the terms of the GNU Affero General Public License as published by the Free   *)
9
(*  Software Foundation, either version 3 of the License, or any later version.   *)
10
(*                                                                                *)
11
(*  Fingerboard is distributed in the hope that it will be useful, but WITHOUT    *)
12
(*  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or         *)
13
(*  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License   *)
14
(*  for more details.                                                             *)
15
(*                                                                                *)
16
(*  You should have received a copy of the GNU Affero General Public License      *)
17
(*  along with Fingerboard. If not, see <https://www.gnu.org/licenses/>.          *)
18
(**********************************************************************************)
19

20
module Vibrating_string : sig
21
  type t =
22
    { open_string : Note.t
23
    ; mutable pitch : Frequency.t
24
    ; roman_numeral : Roman_numeral.t
25
    }
26

27
  val to_dyn : t -> Dyn.t
28
end = struct
29
  type t =
30
    { open_string : Note.t
31
    ; mutable pitch : Frequency.t
32
    ; roman_numeral : Roman_numeral.t
33
    }
34

35
  let to_dyn { open_string; pitch; roman_numeral } =
36
    Dyn.record
61✔
37
      [ "open_string", open_string |> Note.to_dyn
61✔
38
      ; "pitch", pitch |> Frequency.to_dyn
61✔
39
      ; "roman_numeral", roman_numeral |> Roman_numeral.to_dyn
61✔
40
      ]
41
  ;;
42
end
43

44
type t =
45
  { vibrating_strings : Vibrating_string.t array
46
  ; intervals_going_down : Characterized_interval.t array
47
  ; mutable fingerboard_positions : Fingerboard_position.t list
48
  }
49

50
let to_dyn { vibrating_strings; intervals_going_down; fingerboard_positions } =
51
  Dyn.record
15✔
52
    (List.concat
15✔
53
       [ [ "vibrating_strings", vibrating_strings |> Dyn.array Vibrating_string.to_dyn
15✔
54
         ; ( "intervals_going_down"
55
           , intervals_going_down |> Dyn.array Characterized_interval.to_dyn )
15✔
56
         ]
57
       ; (if List.is_empty fingerboard_positions
58
          then []
8✔
59
          else
60
            [ ( "fingerboard_positions"
7✔
61
              , fingerboard_positions |> Dyn.list Fingerboard_position.to_dyn )
7✔
62
            ])
63
       ])
64
;;
65

66
let to_ascii_tables { vibrating_strings; intervals_going_down; fingerboard_positions } =
67
  let vibrating_strings =
7✔
68
    let columns =
69
      Print_table.O.
70
        [ Column.make
7✔
71
            ~align:Right
72
            ~header:"String"
73
            (fun (_, { Vibrating_string.open_string = _; pitch = _; roman_numeral }) ->
74
               Cell.text (Roman_numeral.to_string roman_numeral))
28✔
75
        ; Column.make ~header:"Note" (fun (_, (t : Vibrating_string.t)) ->
7✔
76
            Cell.text (Note.to_string t.open_string))
28✔
77
        ; Column.make ~align:Right ~header:"Pitch" (fun (_, (t : Vibrating_string.t)) ->
7✔
78
            Cell.text (Printf.sprintf "%0.2f" (Frequency.to_float t.pitch)))
28✔
79
        ; Column.make ~header:"Interval" (fun (i, _) ->
7✔
80
            if i >= Array.length intervals_going_down
28✔
81
            then Cell.empty
7✔
82
            else (
21✔
83
              let { Characterized_interval.interval; acoustic_interval } =
84
                intervals_going_down.(i)
85
              in
86
              Cell.text
21✔
87
                (Printf.sprintf
21✔
88
                   "%s - %s"
89
                   (Interval.to_string interval)
21✔
90
                   (Acoustic_interval.to_string acoustic_interval))))
21✔
91
        ; Column.make ~align:Right ~header:"Cents" (fun (i, _) ->
7✔
92
            if i >= Array.length intervals_going_down
28✔
93
            then Cell.empty
7✔
94
            else (
21✔
95
              let { Characterized_interval.acoustic_interval; _ } =
96
                intervals_going_down.(i)
97
              in
98
              Cell.text
21✔
99
                (Cents.to_string_nearest (Acoustic_interval.to_cents acoustic_interval))))
21✔
100
        ]
101
    in
102
    Print_table.make
103
      ~columns
104
      ~rows:(Array.to_list vibrating_strings |> List.mapi ~f:(fun i v -> i, v))
7✔
105
  and fingerboard_positions =
106
    Print_table.make
107
      ~columns:Fingerboard_position.ascii_table_columns
108
      ~rows:fingerboard_positions
109
  in
110
  [ vibrating_strings; fingerboard_positions ]
111
  |> List.map ~f:Print_table.to_string_text
112
  |> String.concat ~sep:"\n"
7✔
113
;;
114

115
let create ~high_vibrating_string ~pitch ~intervals_going_down =
116
  let high_vibrating_string =
117✔
117
    { Vibrating_string.open_string = high_vibrating_string
118
    ; pitch
119
    ; roman_numeral = Roman_numeral.one
120
    }
121
  in
122
  let other_strings =
123
    let previous_string = ref high_vibrating_string in
124
    Array.map
117✔
125
      intervals_going_down
126
      ~f:(fun { Characterized_interval.interval; acoustic_interval } ->
127
        let v =
352✔
128
          let previous_string = !previous_string in
129
          { Vibrating_string.open_string =
130
              previous_string.open_string |> Interval.shift_down interval |> Option.get
352✔
131
          ; pitch =
132
              previous_string.pitch |> Acoustic_interval.shift_down acoustic_interval
352✔
133
          ; roman_numeral = Roman_numeral.succ_exn previous_string.roman_numeral
352✔
134
          }
135
        in
136
        previous_string := v;
137
        v)
138
  in
139
  { vibrating_strings = Array.concat [ [| high_vibrating_string |]; other_strings ]
117✔
140
  ; intervals_going_down
141
  ; fingerboard_positions = []
142
  }
143
;;
144

145
let reset_pitch t roman_numeral ~pitch =
146
  let index = Roman_numeral.to_int roman_numeral |> Int.pred in
3✔
147
  t.vibrating_strings.(index).pitch <- pitch;
3✔
148
  (* Tune going up. *)
149
  for i = index - 1 downto 0 do
150
    t.vibrating_strings.(i).pitch
1✔
151
    <- t.vibrating_strings.(i + 1).pitch
152
       |> Acoustic_interval.shift_up t.intervals_going_down.(i).acoustic_interval
1✔
153
  done;
154
  (* Tune going down. *)
155
  for i = index + 1 to Array.length t.vibrating_strings - 1 do
3✔
156
    t.vibrating_strings.(i).pitch
8✔
157
    <- t.vibrating_strings.(i - 1).pitch
158
       |> Acoustic_interval.shift_down t.intervals_going_down.(i - 1).acoustic_interval
8✔
159
  done;
160
  ()
161
;;
162

163
let vibrating_string_exn (t : t) string_number =
164
  let index = Roman_numeral.to_int string_number - 1 in
1,320,346✔
165
  if index < 0 || index >= Array.length t.vibrating_strings
×
166
  then (
×
167
    let available = Array.map t.vibrating_strings ~f:(fun t -> t.roman_numeral) in
×
168
    Code_error.raise
×
169
      "String number out of bounds."
170
      [ "string_number", string_number |> Roman_numeral.to_dyn
×
171
      ; "available", available |> Dyn.array Roman_numeral.to_dyn
×
172
      ])
173
  else t.vibrating_strings.(index)
1,320,346✔
174
;;
175

176
let pitch (t : t) { Fingerboard_location.fingerboard_position; string_number } =
177
  let vibrating_string = vibrating_string_exn t string_number in
32✔
178
  let interval =
32✔
179
    Fingerboard_position.acoustic_interval_to_the_open_string fingerboard_position
180
  in
181
  Acoustic_interval.shift_up interval vibrating_string.pitch
32✔
182
;;
183

184
let acoustic_interval
185
      (t : t)
186
      ~from:{ Fingerboard_location.fingerboard_position = p1; string_number = s1 }
187
      ~to_:{ Fingerboard_location.fingerboard_position = p2; string_number = s2 }
188
  =
189
  let (_ : Vibrating_string.t) = vibrating_string_exn t s1 in
660,157✔
190
  let (_ : Vibrating_string.t) = vibrating_string_exn t s2 in
660,157✔
191
  let i1 = Roman_numeral.to_int s1
660,157✔
192
  and i2 = Roman_numeral.to_int s2 in
660,157✔
193
  let interval_between_strings = ref Acoustic_interval.unison in
194
  for i = min i1 i2 to max i1 i2 - 1 do
660,157✔
195
    interval_between_strings
644,518✔
196
    := Acoustic_interval.add
644,518✔
197
         !interval_between_strings
198
         t.intervals_going_down.(i - 1).acoustic_interval
199
  done;
200
  Acoustic_interval.remove
201
    (Acoustic_interval.add
660,157✔
202
       !interval_between_strings
203
       (Fingerboard_position.acoustic_interval_to_the_open_string p2))
660,157✔
204
    (Fingerboard_position.acoustic_interval_to_the_open_string p1)
660,157✔
205
;;
206

207
let fingerboard_positions t = t.fingerboard_positions
×
208

209
let find_fingerboard_position (t : t) ~name =
210
  List.find_opt t.fingerboard_positions ~f:(fun fingerboard_position ->
3,858✔
211
    String.equal name (Fingerboard_position.name fingerboard_position))
106,060✔
212
;;
213

214
let find_fingerboard_position_exn t ~name =
215
  match find_fingerboard_position t ~name with
1,282✔
216
  | Some x -> x
1,282✔
217
  | None ->
×
218
    Code_error.raise "Fingerboard_position not found." [ "name", name |> Dyn.string ]
×
219
;;
220

221
module Fingerboard_position_compared_by_acoustic_interval = struct
222
  type t = Fingerboard_position.t
223

224
  let compare (a : t) (b : t) =
225
    Acoustic_interval.compare
330,480✔
226
      (Fingerboard_position.acoustic_interval_to_the_open_string a)
330,480✔
227
      (Fingerboard_position.acoustic_interval_to_the_open_string b)
330,480✔
228
  ;;
229
end
230

231
let add_fingerboard_position_exn
232
      ?(on_n_octaves = 3)
2,576✔
233
      (t : t)
234
      (fingerboard_position : Fingerboard_position.t)
235
  =
236
  let () =
2,576✔
237
    match
238
      Acoustic_interval.compare
239
        (Fingerboard_position.acoustic_interval_to_the_open_string fingerboard_position)
2,576✔
240
        Acoustic_interval.octave
241
    with
NEW
242
    | Lt | Eq -> ()
×
NEW
243
    | Gt ->
×
NEW
244
      Code_error.raise
×
245
        "Interval out of bounds."
NEW
246
        [ "fingerboard_position", fingerboard_position |> Fingerboard_position.to_dyn ]
×
247
  in
248
  let name = Fingerboard_position.name fingerboard_position in
249
  (match find_fingerboard_position t ~name with
2,576✔
250
   | None -> ()
2,576✔
251
   | Some existing_fingerboard_position ->
×
252
     Code_error.raise
×
253
       "Duplicated fingerboard position's name."
254
       [ "name", name |> Dyn.string
×
255
       ; "fingerboard_position", fingerboard_position |> Fingerboard_position.to_dyn
×
256
       ; ( "existing_fingerboard_position"
257
         , existing_fingerboard_position |> Fingerboard_position.to_dyn )
×
258
       ]);
259
  let fingerboard_positions =
260
    List.init on_n_octaves ~f:(fun i ->
2,576✔
261
      Fingerboard_position.at_octave fingerboard_position ~octave:i)
7,728✔
262
    @ t.fingerboard_positions
263
    |> List.sort ~compare:Fingerboard_position_compared_by_acoustic_interval.compare
264
  in
265
  t.fingerboard_positions <- fingerboard_positions
2,576✔
266
;;
267

268
let exists_fingerboard_position t fingerboard_position =
269
  List.exists t.fingerboard_positions ~f:(fun p ->
×
270
    Fingerboard_position.equal p fingerboard_position)
×
271
;;
272

273
let exists_fingerboard_location
274
      t
275
      { Fingerboard_location.fingerboard_position; string_number }
276
  =
277
  let index = Roman_numeral.to_int string_number - 1 in
×
278
  index >= 0
279
  && index < Array.length t.vibrating_strings
×
280
  && exists_fingerboard_position t fingerboard_position
×
281
;;
282

283
let find_next_located_note
284
      (t : t)
285
      { Located_note.note; fingerboard_location }
286
      (characterized_interval : Characterized_interval.t)
287
  =
288
  let ( let* ) x f = Option.bind x ~f in
7,981✔
289
  let index = Roman_numeral.to_int fingerboard_location.string_number in
290
  let* fingerboard_location =
7,981✔
291
    List.find_map (List.init index ~f:Fun.id) ~f:(fun index ->
7,981✔
292
      let string_number = Roman_numeral.of_int_exn (index + 1) in
12,011✔
293
      match
12,011✔
294
        List.find_opt t.fingerboard_positions ~f:(fun fingerboard_position ->
295
          match
495,955✔
296
            acoustic_interval
297
              t
298
              ~from:fingerboard_location
299
              ~to_:{ fingerboard_position; string_number }
300
          with
301
          | None -> false
166,624✔
302
          | Some found_interval ->
329,331✔
303
            Acoustic_interval.equal
304
              found_interval
305
              characterized_interval.acoustic_interval)
306
      with
307
      | None -> None
4,046✔
308
      | Some fingerboard_position ->
7,965✔
309
        Some { Fingerboard_location.fingerboard_position; string_number })
310
  in
311
  let* note = Interval.shift_up characterized_interval.interval note in
7,965✔
312
  Option.some { Located_note.note; fingerboard_location }
7,965✔
313
;;
314

315
let open_string t string_number =
316
  let ( let* ) x f = Option.bind x ~f in
192✔
317
  let index = Roman_numeral.to_int string_number - 1 in
192✔
318
  let* vibrating_string =
319
    if index >= 0 && index < Array.length t.vibrating_strings
192✔
320
    then Option.some t.vibrating_strings.(index)
192✔
321
    else None
×
322
  in
323
  let* fingerboard_position =
192✔
324
    match t.fingerboard_positions with
NEW
325
    | [] -> None
×
326
    | fingerboard_position :: _ ->
192✔
327
      if
328
        Acoustic_interval.equal
329
          Acoustic_interval.unison
330
          (Fingerboard_position.acoustic_interval_to_the_open_string fingerboard_position)
192✔
331
      then Option.some fingerboard_position
192✔
332
      else None
×
333
  in
334
  Option.some
192✔
335
    { Located_note.note = vibrating_string.open_string
336
    ; fingerboard_location = { fingerboard_position; string_number }
337
    }
338
;;
339

340
let make_scale t ~characterized_scale ~from ~to_ =
341
  let rec aux acc scale (located_note : Located_note.t) =
304✔
342
    if Option.is_some (Interval.compute ~from:to_ ~to_:located_note.note ())
9,551✔
343
    then acc
288✔
344
    else (
9,263✔
345
      match scale with
346
      | [] -> aux acc characterized_scale located_note
1,282✔
347
      | hd :: tl ->
7,981✔
348
        (match find_next_located_note t located_note hd with
349
         | None -> acc
16✔
350
         | Some next_located_note -> aux (next_located_note :: acc) tl next_located_note))
7,965✔
351
  in
352
  aux [ from ] [] from |> List.rev
304✔
353
;;
354

355
let find_same_note_one_string_down t { Located_note.note; fingerboard_location } =
356
  let exception No_string_down in
3,557✔
357
  match
358
    let string_number =
359
      let index = Roman_numeral.to_int fingerboard_location.string_number in
360
      if index >= Array.length t.vibrating_strings
3,557✔
361
      then raise_notrace No_string_down
49✔
362
      else Roman_numeral.of_int_exn (index + 1)
3,508✔
363
    in
364
    match
365
      List.find_opt t.fingerboard_positions ~f:(fun fingerboard_position ->
366
        match
133,578✔
367
          acoustic_interval
368
            t
369
            ~from:{ fingerboard_position; string_number }
370
            ~to_:fingerboard_location
371
        with
372
        | None -> false
1,094✔
373
        | Some found_interval ->
132,484✔
374
          Acoustic_interval.equal found_interval Acoustic_interval.unison)
375
    with
376
    | None -> None
40✔
377
    | Some fingerboard_position ->
3,468✔
378
      Some
379
        { Located_note.note
380
        ; fingerboard_location = { fingerboard_position; string_number }
381
        }
382
  with
383
  | res -> res
3,508✔
384
  | exception No_string_down -> None
49✔
385
;;
386

387
module Double_stops = struct
388
  type system = t
389
  type t = Double_stop.t list
390

391
  let to_ascii_table (system : system) double_stops =
392
    let columns =
202✔
393
      let open Print_table.O in
394
      let common_columns ~name ~(f : Double_stop.t -> Located_note.t) =
395
        [ Column.make ~header:name (fun t -> Cell.text (Note.to_string (f t).note))
404✔
396
        ; Column.make ~header:"String" (fun t ->
404✔
397
            Cell.text (Roman_numeral.to_string (f t).fingerboard_location.string_number))
9,462✔
398
        ; Column.make ~header:"Pos" (fun t ->
404✔
399
            Cell.text
9,462✔
400
              (Fingerboard_position.to_string
9,462✔
401
                 (f t).fingerboard_location.fingerboard_position))
9,462✔
402
        ; Column.make ~align:Right ~header:"Cents" (fun t ->
404✔
403
            let acoustic_interval =
9,462✔
404
              Fingerboard_position.acoustic_interval_to_the_open_string
405
                (f t).fingerboard_location.fingerboard_position
9,462✔
406
            in
407
            let cents = Acoustic_interval.to_cents acoustic_interval in
9,462✔
408
            Cell.text (Cents.to_string_nearest cents))
9,462✔
409
        ]
410
      in
411
      [ common_columns ~name:"Low" ~f:(fun (t : Double_stop.t) -> t.low_note)
18,924✔
412
      ; common_columns ~name:"High" ~f:(fun (t : Double_stop.t) -> t.high_note)
18,924✔
413
      ; [ Column.make ~header:"Interval" (fun (t : Double_stop.t) ->
202✔
414
            let interval =
4,731✔
415
              Interval.compute ~from:t.low_note.note ~to_:t.high_note.note ()
416
              |> Option.get
4,731✔
417
            in
418
            let acoustic_interval =
4,731✔
419
              acoustic_interval
420
                system
421
                ~from:t.low_note.fingerboard_location
422
                ~to_:t.high_note.fingerboard_location
423
              |> Option.get
4,731✔
424
            in
425
            Cell.text
4,731✔
426
              (Printf.sprintf
4,731✔
427
                 "%s - %s"
428
                 (Interval.to_string interval)
4,731✔
429
                 (Acoustic_interval.to_string acoustic_interval)))
4,731✔
430
        ; Column.make ~align:Right ~header:"Cents" (fun (t : Double_stop.t) ->
202✔
431
            let acoustic_interval =
4,731✔
432
              acoustic_interval
433
                system
434
                ~from:t.low_note.fingerboard_location
435
                ~to_:t.high_note.fingerboard_location
436
              |> Option.get
4,731✔
437
            in
438
            Cell.text
4,731✔
439
              (Cents.to_string_nearest (Acoustic_interval.to_cents acoustic_interval)))
4,731✔
440
        ]
441
      ]
442
      |> List.concat
202✔
443
    in
444
    Print_table.to_string_text (Print_table.make ~columns ~rows:double_stops)
445
  ;;
446

447
  module Adjustment = struct
448
    type t =
449
      { from : Acoustic_interval.t
450
      ; to_ : Acoustic_interval.t
451
      }
452

453
    module Choice_criteria = struct
454
      (* The type is minted in such a way that the compare function must
455
         prioritize the lower values. *)
456
      type t =
457
        { exists_open_string_with_that_note : bool
458
        ; degree_priority : int
459
        }
460

461
      let compare t { exists_open_string_with_that_note; degree_priority } =
462
        match
38✔
463
          Bool.compare
464
            t.exists_open_string_with_that_note
465
            exists_open_string_with_that_note
466
        with
467
        | (Lt | Gt) as r -> r
12✔
468
        | Eq -> Int.compare t.degree_priority degree_priority
12✔
469
      ;;
470

471
      let of_located_note (system : system) ~tonic (located_note : Located_note.t) =
472
        { exists_open_string_with_that_note =
76✔
473
            Array.exists system.vibrating_strings ~f:(fun v ->
76✔
474
              Note.equal
228✔
475
                v.open_string
476
                { located_note.note with
477
                  octave_designation = v.open_string.octave_designation
478
                })
479
        ; degree_priority =
480
            (match Interval.compute ~from:tonic ~to_:located_note.note () with
481
             | None -> 10
×
482
             | Some interval ->
76✔
483
               (match interval.number with
484
                | Second | Third | Sixth -> 1
×
485
                | Seventh -> 2
×
486
                | Fourth | Fifth -> 3
×
487
                | Unison | Octave -> 4))
×
488
        }
489
      ;;
490
    end
491
  end
492

493
  let adjust (system : system) ~tonic ~adjustment:{ Adjustment.from; to_ } (t : t) =
494
    List.map t ~f:(fun ({ Double_stop.low_note; high_note } as double_stop) ->
56✔
495
      let actual_interval =
1,148✔
496
        acoustic_interval
497
          system
498
          ~from:low_note.fingerboard_location
499
          ~to_:high_note.fingerboard_location
500
        |> Option.get
1,148✔
501
      in
502
      if not (Acoustic_interval.equal actual_interval from)
1,148✔
503
      then double_stop
982✔
504
      else (
166✔
505
        let adjusted_low_note =
506
          let string_number = low_note.fingerboard_location.string_number in
507
          match
508
            List.find_opt system.fingerboard_positions ~f:(fun fingerboard_position ->
509
              match
10,069✔
510
                acoustic_interval
511
                  system
512
                  ~from:{ fingerboard_position; string_number }
513
                  ~to_:high_note.fingerboard_location
514
              with
515
              | None -> false
2,441✔
516
              | Some found_interval -> Acoustic_interval.equal found_interval to_)
7,628✔
517
          with
518
          | None -> None
61✔
519
          | Some fingerboard_position ->
105✔
520
            Some
521
              { low_note with
522
                fingerboard_location = { fingerboard_position; string_number }
523
              }
524
        in
525
        let adjusted_high_note =
526
          let string_number = high_note.fingerboard_location.string_number in
527
          match
528
            List.find_opt system.fingerboard_positions ~f:(fun fingerboard_position ->
529
              match
9,939✔
530
                acoustic_interval
531
                  system
532
                  ~from:low_note.fingerboard_location
533
                  ~to_:{ fingerboard_position; string_number }
534
              with
535
              | None -> false
3,443✔
536
              | Some found_interval -> Acoustic_interval.equal found_interval to_)
6,496✔
537
          with
538
          | None -> None
67✔
539
          | Some fingerboard_position ->
99✔
540
            Some
541
              { high_note with
542
                fingerboard_location = { fingerboard_position; string_number }
543
              }
544
        in
545
        match adjusted_low_note, adjusted_high_note with
546
        | None, None -> (* No adjustment available. *) double_stop
×
547
        | Some low_note, None -> { Double_stop.low_note; high_note }
67✔
548
        | None, Some high_note -> { Double_stop.low_note; high_note }
61✔
549
        | Some adjusted_low_note, Some adjusted_high_note ->
38✔
550
          (match
551
             Adjustment.Choice_criteria.compare
552
               (Adjustment.Choice_criteria.of_located_note system ~tonic low_note)
38✔
553
               (Adjustment.Choice_criteria.of_located_note system ~tonic high_note)
38✔
554
           with
NEW
555
           | Lt | Eq -> { Double_stop.low_note = adjusted_low_note; high_note }
×
556
           | Gt -> { Double_stop.low_note; high_note = adjusted_high_note })))
18✔
557
  ;;
558

559
  let make_scale ?adjustment (t : system) ~characterized_scale ~interval_number ~from ~to_
560
    =
561
    let scale = make_scale t ~characterized_scale ~from ~to_ in
202✔
562
    let double_stops =
202✔
563
      let index = Interval.Number.to_int interval_number - 1 in
202✔
564
      let rec aux acc = function
565
        | [] -> acc
×
566
        | low_note :: tl as scale ->
5,022✔
567
          (match List.nth_opt scale index with
568
           | None -> acc
202✔
569
           | Some high_note ->
4,820✔
570
             let acc =
571
               let double_stop =
572
                 let low_index =
573
                   Roman_numeral.to_int
4,820✔
574
                     low_note.Located_note.fingerboard_location.string_number
575
                 and high_index =
576
                   Roman_numeral.to_int
4,820✔
577
                     high_note.Located_note.fingerboard_location.string_number
578
                 in
579
                 if low_index = high_index
580
                 then
581
                   find_same_note_one_string_down t low_note
582
                   |> Option.map ~f:(fun low_note -> { Double_stop.low_note; high_note })
3,315✔
583
                 else if low_index = high_index + 2
1,416✔
584
                 then
585
                   find_same_note_one_string_down t high_note
586
                   |> Option.map ~f:(fun high_note -> { Double_stop.low_note; high_note })
153✔
587
                 else Some { Double_stop.low_note; high_note }
1,263✔
588
               in
589
               match double_stop with
590
               | None -> acc
89✔
591
               | Some double_stop -> double_stop :: acc
4,731✔
592
             in
593
             aux acc tl)
594
      in
595
      aux [] scale |> List.rev
202✔
596
    in
597
    match adjustment with
598
    | None -> double_stops
146✔
599
    | Some adjustment -> adjust t ~tonic:from.note ~adjustment double_stops
56✔
600
  ;;
601
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc